diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..49fa223cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +#Ignore thumbnails created by Windows +Thumbs.db + +#Ignore files built by Visual Studio +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +.vs/ + +# VS Code/Omnisharp crash dumps +mono_crash.mem*.blob + + +#Nuget packages folder +packages/ + +#Ignore git-related files +*.orig + +#Rider +.idea + +#wwwroot +wwwroot/ + +#npm +node_modules/ +.babelrc +.stylelintrc.yml +.eslintrc.yml + +#github +.github/ + +#volatile +App_Data/ +*.db +*.db-shm +*.db-wal + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# Elsa Stencil Components +src/designer/elsa-workflows-designer/dist +src/designer/elsa-workflows-designer/www +src/designer/elsa-workflows-designer/loader + +# Created by developers using GNU/Linux +.directory diff --git a/Elsa.ProtoActor.sln b/Elsa.ProtoActor.sln new file mode 100644 index 000000000..0dd53bec3 --- /dev/null +++ b/Elsa.ProtoActor.sln @@ -0,0 +1,222 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{61017E64-6D00-49CB-9E81-5002DC8F7D5F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{155227F0-A33B-40AA-A4B4-06F813EB921B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{C6658DE0-2B2F-47F0-BB61-2CA66D435C09}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "activities", "activities", "{F8604289-6D28-428E-9FA8-1480464964F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Console1", "src\samples\console\Elsa.Samples.Console1\Elsa.Samples.Console1.csproj", "{B44A9DDA-8091-490A-BC0B-2EAE4A601B09}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Core", "src\core\Elsa.Core\Elsa.Core.csproj", "{6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Extensions.DependencyInjection", "src\core\Elsa.Extensions.DependencyInjection\Elsa.Extensions.DependencyInjection.csproj", "{042022EA-62BC-44BD-A93F-11ABDB39C791}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D21EB5E-D6B4-48A5-A9BC-A26387A65F0B}" +ProjectSection(SolutionItems) = preProject + README.md = README.md + docker-compose.yml = docker-compose.yml +EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "architecture", "architecture", "{A34FD350-79C5-492A-8811-6DBD80830F8B}" +ProjectSection(SolutionItems) = preProject + docs\architecture\Layer1.md = docs\architecture\Layer1.md + docs\architecture\Layer2.md = docs\architecture\Layer2.md +EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "console", "console", "{873BFC3E-63C2-4495-A503-5EC05DCD84E4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aspnet", "aspnet", "{56C2FFB8-EA54-45B5-A095-4A78142EB4B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Web1", "src\samples\aspnet\Elsa.Samples.Web1\Elsa.Samples.Web1.csproj", "{A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Http", "src\activities\Elsa.Activities.Http\Elsa.Activities.Http.csproj", "{CF88276B-D1C8-4E0D-8043-0BFA54DA693E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{298BA226-B766-40AE-933F-EB9AFCDE051C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Api", "src\api\Elsa.Api\Elsa.Api.csproj", "{39C6C4A2-077E-48C2-B295-DFE357EC38B2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "persistence", "persistence", "{9B7776E2-0014-4C87-B1F4-22FB0BC076F1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.InMemory", "src\persistence\Elsa.Persistence.InMemory\Elsa.Persistence.InMemory.csproj", "{BC97FD3A-1351-443C-9EAE-0F28F8204A6C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "runtime", "runtime", "{04584653-8E42-47DB-850B-7C6C37B307B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Runtime", "src\runtime\Elsa.Runtime\Elsa.Runtime.csproj", "{8C5894A4-F358-45CF-B198-BFBCA30B77F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.Abstractions", "src\persistence\Elsa.Persistence.Abstractions\Elsa.Persistence.Abstractions.csproj", "{C0226CDD-DF79-4767-A4A5-3D9037535929}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Runtime.HostedServices", "src\runtime\Elsa.Runtime.HostedServices\Elsa.Runtime.HostedServices.csproj", "{4A874C29-2915-4F5C-A01A-80F56B929541}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dsl", "dsl", "{CE121DFD-72D4-4DB7-968F-C0DB320D08FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Dsl", "src\dsl\Elsa.Dsl\Elsa.Dsl.csproj", "{61F5C50C-C572-46AB-8F37-54D1F84CC31D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Console2", "src\samples\console\Elsa.Samples.Console2\Elsa.Samples.Console2.csproj", "{48960E35-903C-4EF0-A8F4-3871B000B12D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Timers", "src\activities\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj", "{9FE7C169-F639-4F6D-86EB-D97FE3E9D206}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripting", "scripting", "{2633B8B9-4AEC-4A54-8832-1C940B54E041}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scripting.JavaScript", "src\scripting\Elsa.Scripting.JavaScript\Elsa.Scripting.JavaScript.csproj", "{5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Runtime.ProtoActor", "src\runtime\Elsa.Runtime.ProtoActor\Elsa.Runtime.ProtoActor.csproj", "{E5A05E1D-0F7F-4030-B434-596C76486CDB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Web2", "src\samples\aspnet\Elsa.Samples.Web2\Elsa.Samples.Web2.csproj", "{13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "protoactor", "protoactor", "{03A5D774-A546-4D4B-8687-C6F96A886C72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proto.Actor", "..\..\ProtoActor\protoactor-dotnet\src\Proto.Actor\Proto.Actor.csproj", "{B8F68D35-C561-4C45-881D-84142B557A54}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proto.Cluster", "..\..\ProtoActor\protoactor-dotnet\src\Proto.Cluster\Proto.Cluster.csproj", "{E27E725F-C4B0-4D6E-ADA0-DD7D014F4C6C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proto.Cluster.TestProvider", "..\..\ProtoActor\protoactor-dotnet\src\Proto.Cluster.TestProvider\Proto.Cluster.TestProvider.csproj", "{EB1BACFB-3DF0-45D0-89AF-4571023DDE50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proto.Remote", "..\..\ProtoActor\protoactor-dotnet\src\Proto.Remote\Proto.Remote.csproj", "{0BD81F36-D6B4-4DF1-8BAA-5F25B6779FC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proto.Remote.GrpcCore", "..\..\ProtoActor\protoactor-dotnet\src\Proto.Remote.GrpcCore\Proto.Remote.GrpcCore.csproj", "{466E5047-4184-42C6-9C4D-14A5707E2F49}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Api.Core", "src\api\Elsa.Api.Core\Elsa.Api.Core.csproj", "{A335B51D-8219-4361-BAAF-5F5D1B74EF89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa", "src\core\Elsa\Elsa.csproj", "{521365AE-A681-4E4B-87E6-B4D982CDD520}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Release|Any CPU.Build.0 = Release|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Release|Any CPU.Build.0 = Release|Any CPU + {042022EA-62BC-44BD-A93F-11ABDB39C791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {042022EA-62BC-44BD-A93F-11ABDB39C791}.Debug|Any CPU.Build.0 = Debug|Any CPU + {042022EA-62BC-44BD-A93F-11ABDB39C791}.Release|Any CPU.ActiveCfg = Release|Any CPU + {042022EA-62BC-44BD-A93F-11ABDB39C791}.Release|Any CPU.Build.0 = Release|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Release|Any CPU.Build.0 = Release|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Release|Any CPU.Build.0 = Release|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Release|Any CPU.Build.0 = Release|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Release|Any CPU.Build.0 = Release|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Release|Any CPU.Build.0 = Release|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Release|Any CPU.Build.0 = Release|Any CPU + {4A874C29-2915-4F5C-A01A-80F56B929541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A874C29-2915-4F5C-A01A-80F56B929541}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A874C29-2915-4F5C-A01A-80F56B929541}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A874C29-2915-4F5C-A01A-80F56B929541}.Release|Any CPU.Build.0 = Release|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Release|Any CPU.Build.0 = Release|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Release|Any CPU.Build.0 = Release|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Release|Any CPU.Build.0 = Release|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Release|Any CPU.Build.0 = Release|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Release|Any CPU.Build.0 = Release|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Release|Any CPU.Build.0 = Release|Any CPU + {B8F68D35-C561-4C45-881D-84142B557A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8F68D35-C561-4C45-881D-84142B557A54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8F68D35-C561-4C45-881D-84142B557A54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8F68D35-C561-4C45-881D-84142B557A54}.Release|Any CPU.Build.0 = Release|Any CPU + {E27E725F-C4B0-4D6E-ADA0-DD7D014F4C6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E27E725F-C4B0-4D6E-ADA0-DD7D014F4C6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E27E725F-C4B0-4D6E-ADA0-DD7D014F4C6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E27E725F-C4B0-4D6E-ADA0-DD7D014F4C6C}.Release|Any CPU.Build.0 = Release|Any CPU + {EB1BACFB-3DF0-45D0-89AF-4571023DDE50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB1BACFB-3DF0-45D0-89AF-4571023DDE50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB1BACFB-3DF0-45D0-89AF-4571023DDE50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB1BACFB-3DF0-45D0-89AF-4571023DDE50}.Release|Any CPU.Build.0 = Release|Any CPU + {0BD81F36-D6B4-4DF1-8BAA-5F25B6779FC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BD81F36-D6B4-4DF1-8BAA-5F25B6779FC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BD81F36-D6B4-4DF1-8BAA-5F25B6779FC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BD81F36-D6B4-4DF1-8BAA-5F25B6779FC5}.Release|Any CPU.Build.0 = Release|Any CPU + {466E5047-4184-42C6-9C4D-14A5707E2F49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {466E5047-4184-42C6-9C4D-14A5707E2F49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {466E5047-4184-42C6-9C4D-14A5707E2F49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {466E5047-4184-42C6-9C4D-14A5707E2F49}.Release|Any CPU.Build.0 = Release|Any CPU + {A335B51D-8219-4361-BAAF-5F5D1B74EF89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A335B51D-8219-4361-BAAF-5F5D1B74EF89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A335B51D-8219-4361-BAAF-5F5D1B74EF89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A335B51D-8219-4361-BAAF-5F5D1B74EF89}.Release|Any CPU.Build.0 = Release|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {F8604289-6D28-428E-9FA8-1480464964F0} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {042022EA-62BC-44BD-A93F-11ABDB39C791} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {A34FD350-79C5-492A-8811-6DBD80830F8B} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6} + {873BFC3E-63C2-4495-A503-5EC05DCD84E4} = {155227F0-A33B-40AA-A4B4-06F813EB921B} + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09} = {873BFC3E-63C2-4495-A503-5EC05DCD84E4} + {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} = {155227F0-A33B-40AA-A4B4-06F813EB921B} + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E} = {F8604289-6D28-428E-9FA8-1480464964F0} + {298BA226-B766-40AE-933F-EB9AFCDE051C} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {39C6C4A2-077E-48C2-B295-DFE357EC38B2} = {298BA226-B766-40AE-933F-EB9AFCDE051C} + {9B7776E2-0014-4C87-B1F4-22FB0BC076F1} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C} = {9B7776E2-0014-4C87-B1F4-22FB0BC076F1} + {04584653-8E42-47DB-850B-7C6C37B307B7} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {8C5894A4-F358-45CF-B198-BFBCA30B77F6} = {04584653-8E42-47DB-850B-7C6C37B307B7} + {C0226CDD-DF79-4767-A4A5-3D9037535929} = {9B7776E2-0014-4C87-B1F4-22FB0BC076F1} + {4A874C29-2915-4F5C-A01A-80F56B929541} = {04584653-8E42-47DB-850B-7C6C37B307B7} + {CE121DFD-72D4-4DB7-968F-C0DB320D08FB} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {61F5C50C-C572-46AB-8F37-54D1F84CC31D} = {CE121DFD-72D4-4DB7-968F-C0DB320D08FB} + {48960E35-903C-4EF0-A8F4-3871B000B12D} = {873BFC3E-63C2-4495-A503-5EC05DCD84E4} + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206} = {F8604289-6D28-428E-9FA8-1480464964F0} + {2633B8B9-4AEC-4A54-8832-1C940B54E041} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9} = {2633B8B9-4AEC-4A54-8832-1C940B54E041} + {E5A05E1D-0F7F-4030-B434-596C76486CDB} = {04584653-8E42-47DB-850B-7C6C37B307B7} + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {B8F68D35-C561-4C45-881D-84142B557A54} = {03A5D774-A546-4D4B-8687-C6F96A886C72} + {E27E725F-C4B0-4D6E-ADA0-DD7D014F4C6C} = {03A5D774-A546-4D4B-8687-C6F96A886C72} + {EB1BACFB-3DF0-45D0-89AF-4571023DDE50} = {03A5D774-A546-4D4B-8687-C6F96A886C72} + {0BD81F36-D6B4-4DF1-8BAA-5F25B6779FC5} = {03A5D774-A546-4D4B-8687-C6F96A886C72} + {466E5047-4184-42C6-9C4D-14A5707E2F49} = {03A5D774-A546-4D4B-8687-C6F96A886C72} + {A335B51D-8219-4361-BAAF-5F5D1B74EF89} = {298BA226-B766-40AE-933F-EB9AFCDE051C} + {521365AE-A681-4E4B-87E6-B4D982CDD520} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + EndGlobalSection +EndGlobal diff --git a/Elsa.sln b/Elsa.sln new file mode 100644 index 000000000..bb551fd90 --- /dev/null +++ b/Elsa.sln @@ -0,0 +1,202 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{61017E64-6D00-49CB-9E81-5002DC8F7D5F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{155227F0-A33B-40AA-A4B4-06F813EB921B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{C6658DE0-2B2F-47F0-BB61-2CA66D435C09}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "activities", "activities", "{F8604289-6D28-428E-9FA8-1480464964F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Console1", "src\samples\console\Elsa.Samples.Console1\Elsa.Samples.Console1.csproj", "{B44A9DDA-8091-490A-BC0B-2EAE4A601B09}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Core", "src\core\Elsa.Core\Elsa.Core.csproj", "{6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D21EB5E-D6B4-48A5-A9BC-A26387A65F0B}" +ProjectSection(SolutionItems) = preProject + README.md = README.md + docker-compose.yml = docker-compose.yml +EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "architecture", "architecture", "{A34FD350-79C5-492A-8811-6DBD80830F8B}" +ProjectSection(SolutionItems) = preProject + docs\architecture\Layer1.md = docs\architecture\Layer1.md + docs\architecture\Layer2.md = docs\architecture\Layer2.md +EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "console", "console", "{873BFC3E-63C2-4495-A503-5EC05DCD84E4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aspnet", "aspnet", "{56C2FFB8-EA54-45B5-A095-4A78142EB4B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Web1", "src\samples\aspnet\Elsa.Samples.Web1\Elsa.Samples.Web1.csproj", "{A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Http", "src\activities\Elsa.Activities.Http\Elsa.Activities.Http.csproj", "{CF88276B-D1C8-4E0D-8043-0BFA54DA693E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{298BA226-B766-40AE-933F-EB9AFCDE051C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Api", "src\api\Elsa.Api\Elsa.Api.csproj", "{39C6C4A2-077E-48C2-B295-DFE357EC38B2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "persistence", "persistence", "{9B7776E2-0014-4C87-B1F4-22FB0BC076F1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.InMemory", "src\persistence\Elsa.Persistence.InMemory\Elsa.Persistence.InMemory.csproj", "{BC97FD3A-1351-443C-9EAE-0F28F8204A6C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "runtime", "runtime", "{04584653-8E42-47DB-850B-7C6C37B307B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Runtime", "src\runtime\Elsa.Runtime\Elsa.Runtime.csproj", "{8C5894A4-F358-45CF-B198-BFBCA30B77F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.Abstractions", "src\persistence\Elsa.Persistence.Abstractions\Elsa.Persistence.Abstractions.csproj", "{C0226CDD-DF79-4767-A4A5-3D9037535929}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dsl", "dsl", "{CE121DFD-72D4-4DB7-968F-C0DB320D08FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Dsl", "src\dsl\Elsa.Dsl\Elsa.Dsl.csproj", "{61F5C50C-C572-46AB-8F37-54D1F84CC31D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Console2", "src\samples\console\Elsa.Samples.Console2\Elsa.Samples.Console2.csproj", "{48960E35-903C-4EF0-A8F4-3871B000B12D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Timers", "src\activities\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj", "{9FE7C169-F639-4F6D-86EB-D97FE3E9D206}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripting", "scripting", "{2633B8B9-4AEC-4A54-8832-1C940B54E041}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scripting.JavaScript", "src\scripting\Elsa.Scripting.JavaScript\Elsa.Scripting.JavaScript.csproj", "{5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Runtime.ProtoActor", "src\runtime\Elsa.Runtime.ProtoActor\Elsa.Runtime.ProtoActor.csproj", "{E5A05E1D-0F7F-4030-B434-596C76486CDB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Web2", "src\samples\aspnet\Elsa.Samples.Web2\Elsa.Samples.Web2.csproj", "{13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa", "src\core\Elsa\Elsa.csproj", "{521365AE-A681-4E4B-87E6-B4D982CDD520}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Mediator", "src\core\Elsa.Mediator\Elsa.Mediator.csproj", "{5CC45C6C-0751-4A33-8306-758BB17417F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Management", "src\core\Elsa.Management\Elsa.Management.csproj", "{03C2D8B9-C61F-4750-8107-765E3FE02ADA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "efcore", "efcore", "{9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EntityFrameworkCore", "src\persistence\Elsa.Persistence.EntityFrameworkCore\Elsa.Persistence.EntityFrameworkCore.csproj", "{D8130816-C1D9-40A3-9079-F2887FE35EA9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EntityFrameworkCore.Sqlite", "src\persistence\Elsa.Persistence.EntityFrameworkCore.Sqlite\Elsa.Persistence.EntityFrameworkCore.Sqlite.csproj", "{820016B7-01CD-4032-8239-6A4E53F1A352}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Serialization", "src\core\Elsa.Serialization\Elsa.Serialization.csproj", "{55F9B33D-5FAF-4355-97EF-445C93208E85}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09}.Release|Any CPU.Build.0 = Release|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9}.Release|Any CPU.Build.0 = Release|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678}.Release|Any CPU.Build.0 = Release|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E}.Release|Any CPU.Build.0 = Release|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39C6C4A2-077E-48C2-B295-DFE357EC38B2}.Release|Any CPU.Build.0 = Release|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C}.Release|Any CPU.Build.0 = Release|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C5894A4-F358-45CF-B198-BFBCA30B77F6}.Release|Any CPU.Build.0 = Release|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0226CDD-DF79-4767-A4A5-3D9037535929}.Release|Any CPU.Build.0 = Release|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61F5C50C-C572-46AB-8F37-54D1F84CC31D}.Release|Any CPU.Build.0 = Release|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48960E35-903C-4EF0-A8F4-3871B000B12D}.Release|Any CPU.Build.0 = Release|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206}.Release|Any CPU.Build.0 = Release|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9}.Release|Any CPU.Build.0 = Release|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5A05E1D-0F7F-4030-B434-596C76486CDB}.Release|Any CPU.Build.0 = Release|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF}.Release|Any CPU.Build.0 = Release|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {521365AE-A681-4E4B-87E6-B4D982CDD520}.Release|Any CPU.Build.0 = Release|Any CPU + {5CC45C6C-0751-4A33-8306-758BB17417F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CC45C6C-0751-4A33-8306-758BB17417F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CC45C6C-0751-4A33-8306-758BB17417F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CC45C6C-0751-4A33-8306-758BB17417F7}.Release|Any CPU.Build.0 = Release|Any CPU + {03C2D8B9-C61F-4750-8107-765E3FE02ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03C2D8B9-C61F-4750-8107-765E3FE02ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03C2D8B9-C61F-4750-8107-765E3FE02ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03C2D8B9-C61F-4750-8107-765E3FE02ADA}.Release|Any CPU.Build.0 = Release|Any CPU + {D8130816-C1D9-40A3-9079-F2887FE35EA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8130816-C1D9-40A3-9079-F2887FE35EA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8130816-C1D9-40A3-9079-F2887FE35EA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8130816-C1D9-40A3-9079-F2887FE35EA9}.Release|Any CPU.Build.0 = Release|Any CPU + {820016B7-01CD-4032-8239-6A4E53F1A352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {820016B7-01CD-4032-8239-6A4E53F1A352}.Debug|Any CPU.Build.0 = Debug|Any CPU + {820016B7-01CD-4032-8239-6A4E53F1A352}.Release|Any CPU.ActiveCfg = Release|Any CPU + {820016B7-01CD-4032-8239-6A4E53F1A352}.Release|Any CPU.Build.0 = Release|Any CPU + {55F9B33D-5FAF-4355-97EF-445C93208E85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55F9B33D-5FAF-4355-97EF-445C93208E85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55F9B33D-5FAF-4355-97EF-445C93208E85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55F9B33D-5FAF-4355-97EF-445C93208E85}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {F8604289-6D28-428E-9FA8-1480464964F0} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {A34FD350-79C5-492A-8811-6DBD80830F8B} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6} + {873BFC3E-63C2-4495-A503-5EC05DCD84E4} = {155227F0-A33B-40AA-A4B4-06F813EB921B} + {B44A9DDA-8091-490A-BC0B-2EAE4A601B09} = {873BFC3E-63C2-4495-A503-5EC05DCD84E4} + {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} = {155227F0-A33B-40AA-A4B4-06F813EB921B} + {A2E6E9A4-4C21-4022-A53E-BDF5ED0D0678} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {CF88276B-D1C8-4E0D-8043-0BFA54DA693E} = {F8604289-6D28-428E-9FA8-1480464964F0} + {298BA226-B766-40AE-933F-EB9AFCDE051C} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {39C6C4A2-077E-48C2-B295-DFE357EC38B2} = {298BA226-B766-40AE-933F-EB9AFCDE051C} + {9B7776E2-0014-4C87-B1F4-22FB0BC076F1} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {BC97FD3A-1351-443C-9EAE-0F28F8204A6C} = {9B7776E2-0014-4C87-B1F4-22FB0BC076F1} + {04584653-8E42-47DB-850B-7C6C37B307B7} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {8C5894A4-F358-45CF-B198-BFBCA30B77F6} = {04584653-8E42-47DB-850B-7C6C37B307B7} + {C0226CDD-DF79-4767-A4A5-3D9037535929} = {9B7776E2-0014-4C87-B1F4-22FB0BC076F1} + {CE121DFD-72D4-4DB7-968F-C0DB320D08FB} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {61F5C50C-C572-46AB-8F37-54D1F84CC31D} = {CE121DFD-72D4-4DB7-968F-C0DB320D08FB} + {48960E35-903C-4EF0-A8F4-3871B000B12D} = {873BFC3E-63C2-4495-A503-5EC05DCD84E4} + {9FE7C169-F639-4F6D-86EB-D97FE3E9D206} = {F8604289-6D28-428E-9FA8-1480464964F0} + {2633B8B9-4AEC-4A54-8832-1C940B54E041} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {5D349665-3B5D-4DA4-A7B0-C6F11611B0C9} = {2633B8B9-4AEC-4A54-8832-1C940B54E041} + {E5A05E1D-0F7F-4030-B434-596C76486CDB} = {04584653-8E42-47DB-850B-7C6C37B307B7} + {13A6BAF1-9BAD-4A3A-A61F-5E4F01CDF0BF} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {521365AE-A681-4E4B-87E6-B4D982CDD520} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {5CC45C6C-0751-4A33-8306-758BB17417F7} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {03C2D8B9-C61F-4750-8107-765E3FE02ADA} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA} = {9B7776E2-0014-4C87-B1F4-22FB0BC076F1} + {D8130816-C1D9-40A3-9079-F2887FE35EA9} = {9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA} + {820016B7-01CD-4032-8239-6A4E53F1A352} = {9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA} + {55F9B33D-5FAF-4355-97EF-445C93208E85} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + EndGlobalSection +EndGlobal diff --git a/Elsa.sln.DotSettings b/Elsa.sln.DotSettings new file mode 100644 index 000000000..05eadb4ff --- /dev/null +++ b/Elsa.sln.DotSettings @@ -0,0 +1,9 @@ + + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..ed7211fb7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Elsa Workflows + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 000000000..b879d3722 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..1b8912adf --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# experimental \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..3c1e24fac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.7' +services: + + consul-agent-1: &consul-agent + image: consul:latest + networks: + - consul + command: "agent -retry-join consul-server-bootstrap -client 0.0.0.0" + + consul-agent-2: + <<: *consul-agent + + consul-agent-3: + <<: *consul-agent + + consul-server-1: &consul-server + <<: *consul-agent + command: "agent -server -retry-join consul-server-bootstrap -client 0.0.0.0" + + consul-server-2: + <<: *consul-server + + consul-server-bootstrap: + <<: *consul-agent + ports: + - "8400:8400" + - "8500:8500" + - "8600:8600" + - "8600:8600/udp" + command: "agent -server -bootstrap-expect 3 -ui -client 0.0.0.0" + + mongodb: + image: mongo:latest + ports: + - 127.0.0.1:27017:27017 + volumes: + - mongodb_data:/data/db + + redis: + image: redis:latest + ports: + - 127.0.0.1:6379:6379 + +networks: + consul: + +volumes: + mongodb_data: \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj new file mode 100644 index 000000000..2f8065e2f --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + Latest + + + + + + + + + + + + diff --git a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj.DotSettings b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj.DotSettings new file mode 100644 index 000000000..875c6b6a6 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj.DotSettings @@ -0,0 +1,4 @@ + + True + True + True \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Extensions/ApplicationBuilderExtensions.cs b/src/activities/Elsa.Activities.Http/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 000000000..dd92bfc4c --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,9 @@ +using Elsa.Activities.Http.Middleware; +using Microsoft.AspNetCore.Builder; + +namespace Elsa.Activities.Http.Extensions; + +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder UseHttpActivities(this IApplicationBuilder app) => app.UseMiddleware(); +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..58f9f60bc --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ + + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddHttpActivityServices(this IServiceCollection services) + { + return services + .AddHttpContextAccessor(); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/HttpTrigger.cs b/src/activities/Elsa.Activities.Http/HttpTrigger.cs new file mode 100644 index 000000000..117ca9a91 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/HttpTrigger.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Management.Models; +using Elsa.Models; + +namespace Elsa.Activities.Http; + +public class HttpTrigger : Trigger +{ + [Input] public Input Path { get; set; } = default!; + + [Input( + Options = new[] { "GET", "POST", "PUT" }, + UIHint = InputUIHints.CheckList + )] + public Input> SupportedMethods { get; set; } = new(new[] { HttpMethod.Get.Method }); + + [Output] public Output? Result { get; set; } + + protected override IEnumerable GetHashInputs(TriggerIndexingContext context) + { + var path = context.ExpressionExecutionContext.Get(Path); + var methods = context.ExpressionExecutionContext.Get(SupportedMethods); + return methods!.Select(x => (path!.ToLowerInvariant(), x.ToLowerInvariant())).Cast().ToArray(); + } + + protected override void Execute(ActivityExecutionContext context) + { + var bookmarks = CreateBookmarks(context).ToList(); + context.SetBookmarks(bookmarks); + } + + private IEnumerable CreateBookmarks(ActivityExecutionContext context) + { + var path = context.Get(Path)!; + var methods = context.Get(SupportedMethods)!; + var hasher = context.GetRequiredService(); + var identityGenerator = context.GetRequiredService(); + + foreach (var method in methods) + { + var hashInput = (path.ToLowerInvariant(), method.ToLowerInvariant()); + var hash = hasher.Hash(hashInput); + var bookmarkId = identityGenerator.GenerateId(); + yield return new Bookmark(bookmarkId, NodeType, hash, Id, context.Id); + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Middleware/HttpTriggerMiddleware.cs b/src/activities/Elsa.Activities.Http/Middleware/HttpTriggerMiddleware.cs new file mode 100644 index 000000000..d4a1510ff --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Middleware/HttpTriggerMiddleware.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Elsa.Contracts; +using Elsa.Helpers; +using Elsa.Runtime.Contracts; +using Elsa.Runtime.Models; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Activities.Http.Middleware; + +public class HttpTriggerMiddleware +{ + private readonly RequestDelegate _next; + private readonly IHasher _hasher; + + public HttpTriggerMiddleware(RequestDelegate next, IHasher hasher) + { + _next = next; + _hasher = hasher; + } + + public async Task InvokeAsync(HttpContext httpContext, IWorkflowServer workflowServer) + { + var path = GetPath(httpContext); + var method = httpContext.Request.Method!.ToLowerInvariant(); + var abortToken = httpContext.RequestAborted; + var hash = _hasher.Hash((path.ToLowerInvariant(), method.ToLowerInvariant())); + var activityTypeName = TypeNameHelper.GenerateTypeName(); + var stimulus = Stimuli.Standard(activityTypeName, hash); + var executionResults = (await workflowServer.ExecuteStimulusAsync(stimulus, abortToken)).ToList(); + + if (!executionResults.Any()) + { + await _next(httpContext); + return; + } + + var response = httpContext.Response; + + if (!response.HasStarted) + { + response.ContentType = "application/json"; + response.StatusCode = StatusCodes.Status200OK; + + var model = new + { + workflowInstanceIds = executionResults.Select(x => x.ExecuteWorkflowResult.WorkflowState.Id).ToArray() + }; + + var json = JsonSerializer.Serialize(model); + await response.WriteAsync(json, abortToken); + } + } + + private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value.ToLowerInvariant(); +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Models/HttpRequestModel.cs b/src/activities/Elsa.Activities.Http/Models/HttpRequestModel.cs new file mode 100644 index 000000000..201589e57 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Models/HttpRequestModel.cs @@ -0,0 +1,8 @@ +using System; + +namespace Elsa.Activities.Http; + +public class HttpRequestModel +{ + public Uri Url { get; set; } = default!; +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/WriteHttpResponse.cs b/src/activities/Elsa.Activities.Http/WriteHttpResponse.cs new file mode 100644 index 000000000..4d41bf7c2 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/WriteHttpResponse.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Elsa.Models; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Activities.Http; + +public class WriteHttpResponse : Activity +{ + public Input StatusCode { get; set; } = new(HttpStatusCode.OK); + public Input Content { get; set; } = new(""); + + public override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var httpContextAccessor = context.GetRequiredService(); + var httpContext = httpContextAccessor.HttpContext; + var response = httpContext.Response; + + response.StatusCode = (int)context.Get(StatusCode); + + var content = context.Get(Content); + + if (content != null) + await response.WriteAsync(content, context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Timers/AssemblyInfo.cs b/src/activities/Elsa.Activities.Timers/AssemblyInfo.cs new file mode 100644 index 000000000..e46bb2692 --- /dev/null +++ b/src/activities/Elsa.Activities.Timers/AssemblyInfo.cs @@ -0,0 +1,2 @@ +using System; +[assembly: CLSCompliant(true)] \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Timers/Elsa.Activities.Timers.csproj b/src/activities/Elsa.Activities.Timers/Elsa.Activities.Timers.csproj new file mode 100644 index 000000000..6e698355e --- /dev/null +++ b/src/activities/Elsa.Activities.Timers/Elsa.Activities.Timers.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + Latest + + + + + + + diff --git a/src/activities/Elsa.Activities.Timers/TimerTrigger.cs b/src/activities/Elsa.Activities.Timers/TimerTrigger.cs new file mode 100644 index 000000000..be639d28a --- /dev/null +++ b/src/activities/Elsa.Activities.Timers/TimerTrigger.cs @@ -0,0 +1,10 @@ +using System; +using Elsa.Attributes; +using Elsa.Models; + +namespace Elsa.Activities.Timers; + +public class TimerTrigger : Trigger +{ + [Input] public Input Interval { get; set; } = default!; +} \ No newline at end of file diff --git a/src/api/Elsa.Api/ApiResults/DispatchWorkflowResult.cs b/src/api/Elsa.Api/ApiResults/DispatchWorkflowResult.cs new file mode 100644 index 000000000..3998ce585 --- /dev/null +++ b/src/api/Elsa.Api/ApiResults/DispatchWorkflowResult.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Elsa.Models; +using Elsa.Runtime.Contracts; +using Elsa.Runtime.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Api.ApiResults; + +public class DispatchWorkflowResult : IResult +{ + public DispatchWorkflowResult(Workflow workflow) => Workflow = workflow; + public Workflow Workflow { get; } + + public async Task ExecuteAsync(HttpContext httpContext) + { + var response = httpContext.Response; + var workflowInvoker = httpContext.RequestServices.GetRequiredService(); + var (definitionId, version, _) = Workflow.Identity; + var result = await workflowInvoker.DispatchAsync(new DispatchWorkflowDefinitionRequest(definitionId, version)); + + response.StatusCode = (int)HttpStatusCode.OK; + await response.WriteAsJsonAsync(result, httpContext.RequestAborted); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/ApiResults/EmptyResult.cs b/src/api/Elsa.Api/ApiResults/EmptyResult.cs new file mode 100644 index 000000000..9c59a2559 --- /dev/null +++ b/src/api/Elsa.Api/ApiResults/EmptyResult.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.ApiResults; + +public class EmptyResult : IResult +{ + public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; +} \ No newline at end of file diff --git a/src/api/Elsa.Api/ApiResults/ExecuteWorkflowResult.cs b/src/api/Elsa.Api/ApiResults/ExecuteWorkflowResult.cs new file mode 100644 index 000000000..0dc64c2ab --- /dev/null +++ b/src/api/Elsa.Api/ApiResults/ExecuteWorkflowResult.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Models; +using Elsa.Runtime.Contracts; +using Elsa.Runtime.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Api.ApiResults; + +public class ExecuteWorkflowResult : IResult +{ + public ExecuteWorkflowResult(Workflow workflow) => Workflow = workflow; + public Workflow Workflow { get; } + + public async Task ExecuteAsync(HttpContext httpContext) + { + var response = httpContext.Response; + var workflowInvoker = httpContext.RequestServices.GetRequiredService(); + var (definitionId, version, _) = Workflow.Identity; + var executeRequest = new ExecuteWorkflowDefinitionRequest(definitionId, version); + var result = await workflowInvoker.ExecuteAsync(executeRequest, CancellationToken.None); + + if (!response.HasStarted) + await response.WriteAsJsonAsync(result, httpContext.RequestAborted); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/ApiResults/ProcessStimulusResult.cs b/src/api/Elsa.Api/ApiResults/ProcessStimulusResult.cs new file mode 100644 index 000000000..52a94e04b --- /dev/null +++ b/src/api/Elsa.Api/ApiResults/ProcessStimulusResult.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Runtime.Contracts; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Api.ApiResults; + +public class ProcessStimulusResult : IResult +{ + public ProcessStimulusResult(IStimulus stimulus) + { + Stimulus = stimulus; + } + + public IStimulus Stimulus { get; } + + public async Task ExecuteAsync(HttpContext httpContext) + { + var response = httpContext.Response; + var stimulusInterpreter = httpContext.RequestServices.GetRequiredService(); + var abortToken = httpContext.RequestAborted; + var instructions = await stimulusInterpreter.GetExecutionInstructionsAsync(Stimulus, abortToken); + var instructionExecutor = httpContext.RequestServices.GetRequiredService(); + var executionResults = (await instructionExecutor.ExecuteInstructionsAsync(instructions, CancellationToken.None)).ToList(); + + if (!response.HasStarted) + { + var model = new + { + Results = executionResults + }; + + await response.WriteAsJsonAsync(model, httpContext.RequestAborted); + } + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Elsa.Api.csproj b/src/api/Elsa.Api/Elsa.Api.csproj new file mode 100644 index 000000000..092f8af5b --- /dev/null +++ b/src/api/Elsa.Api/Elsa.Api.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + preview + + + + + + + + + + + + + + + + diff --git a/src/api/Elsa.Api/Endpoints/ActivityDescriptors/List.cs b/src/api/Elsa.Api/Endpoints/ActivityDescriptors/List.cs new file mode 100644 index 000000000..e000b5c99 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/ActivityDescriptors/List.cs @@ -0,0 +1,28 @@ +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Management.Contracts; +using Elsa.Management.Serialization.Converters; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Endpoints.ActivityDescriptors; + +public static partial class ActivityDescriptors +{ + public static IResult ListAsync(IActivityRegistry registry, IWellKnownTypeRegistry wellKnownTypeRegistry) + { + var descriptors = registry.ListAll().ToList(); + + var model = new + { + ActivityDescriptors = descriptors + }; + + return Results.Json(model, new JsonSerializerOptions + { + Converters = { new TypeJsonConverter(wellKnownTypeRegistry) }, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/ActivityDescriptors/Resource.cs b/src/api/Elsa.Api/Endpoints/ActivityDescriptors/Resource.cs new file mode 100644 index 000000000..3531d6a0d --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/ActivityDescriptors/Resource.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Api.Endpoints.ActivityDescriptors; + +public class ActivityDescriptorsResource +{ + private readonly IEndpointRouteBuilder _endpoints; + + public ActivityDescriptorsResource(IEndpointRouteBuilder endpoints) + { + _endpoints = endpoints; + } + + public ActivityDescriptorsResource List() + { + _endpoints.MapGet("api/descriptors/activities", ActivityDescriptors.ListAsync); + return this; + } +} + +public static class ActivityDescriptorsResourceExtensions +{ + public static IEndpointRouteBuilder MapActivityDescriptors(this IEndpointRouteBuilder endpoints, Action configureResource) + { + configureResource(new(endpoints)); + return endpoints; + } + + public static IEndpointRouteBuilder MapActivityDescriptors(this IEndpointRouteBuilder endpoints) => endpoints.MapActivityDescriptors(x => x.List()); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Events/Resource.cs b/src/api/Elsa.Api/Endpoints/Events/Resource.cs new file mode 100644 index 000000000..5089bf761 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Events/Resource.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Api.Endpoints.Events; + +public class EventsResource +{ + private readonly IEndpointRouteBuilder _endpoints; + + public EventsResource(IEndpointRouteBuilder endpoints) + { + _endpoints = endpoints; + } + + public EventsResource Trigger() + { + _endpoints.MapPost("api/events/{eventName}/trigger", Events.Trigger.Handle); + return this; + } +} + +public static class EventsResourceExtensions +{ + public static IEndpointRouteBuilder MapEvents(this IEndpointRouteBuilder endpoints, Action configureResource) + { + configureResource(new(endpoints)); + return endpoints; + } + + public static IEndpointRouteBuilder MapEvents(this IEndpointRouteBuilder endpoints) => endpoints.MapEvents(x => x.Trigger()); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Events/Trigger.cs b/src/api/Elsa.Api/Endpoints/Events/Trigger.cs new file mode 100644 index 000000000..1b1db978a --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Events/Trigger.cs @@ -0,0 +1,17 @@ +using Elsa.Activities.Primitives; +using Elsa.Api.ApiResults; +using Elsa.Contracts; +using Elsa.Runtime.Models; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Endpoints.Events; + +public static class Trigger +{ + public static IResult Handle(string eventName, IHasher hasher) + { + var hash = hasher.Hash(eventName); + var stimulus = Stimuli.Standard(nameof(Event), hash); + return new ProcessStimulusResult(stimulus); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/TriggerDescriptors/List.cs b/src/api/Elsa.Api/Endpoints/TriggerDescriptors/List.cs new file mode 100644 index 000000000..5cf232411 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/TriggerDescriptors/List.cs @@ -0,0 +1,28 @@ +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Management.Contracts; +using Elsa.Management.Serialization.Converters; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Endpoints.TriggerDescriptors; + +public static partial class TriggerDescriptors +{ + public static IResult ListAsync(ITriggerRegistry registry, IWellKnownTypeRegistry wellKnownTypeRegistry) + { + var descriptors = registry.ListAll().ToList(); + + var model = new + { + TriggerDescriptors = descriptors + }; + + return Results.Json(model, new JsonSerializerOptions + { + Converters = { new TypeJsonConverter(wellKnownTypeRegistry) }, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/TriggerDescriptors/Resource.cs b/src/api/Elsa.Api/Endpoints/TriggerDescriptors/Resource.cs new file mode 100644 index 000000000..38b65aeb2 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/TriggerDescriptors/Resource.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Api.Endpoints.TriggerDescriptors; + +public class TriggerDescriptorsResource +{ + private readonly IEndpointRouteBuilder _endpoints; + + public TriggerDescriptorsResource(IEndpointRouteBuilder endpoints) + { + _endpoints = endpoints; + } + + public TriggerDescriptorsResource List() + { + _endpoints.MapGet("api/descriptors/triggers", TriggerDescriptors.ListAsync); + return this; + } +} + +public static class TriggerDescriptorsResourceExtensions +{ + public static IEndpointRouteBuilder MapTriggerDescriptors(this IEndpointRouteBuilder endpoints, Action configureResource) + { + configureResource(new(endpoints)); + return endpoints; + } + + public static IEndpointRouteBuilder MapTriggerDescriptors(this IEndpointRouteBuilder endpoints) => endpoints.MapTriggerDescriptors(x => x.List()); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/WorkflowInstances/Get.cs b/src/api/Elsa.Api/Endpoints/WorkflowInstances/Get.cs new file mode 100644 index 000000000..aa61cf7ea --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowInstances/Get.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Management.Serialization; +using Elsa.Mediator.Contracts; +using Elsa.Persistence.Requests; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Endpoints.WorkflowInstances; + +public static partial class WorkflowInstances +{ + public static async Task GetAsync(IRequestSender requestSender, WorkflowSerializerOptionsProvider serializerOptionsProvider, string id, CancellationToken cancellationToken) + { + var serializerOptions = serializerOptionsProvider.CreateSerializerOptions(); + var request = new FindWorkflowInstance(id); + var workflowInstance = await requestSender.RequestAsync(request, cancellationToken); + return workflowInstance != null ? Results.Json(workflowInstance, serializerOptions, statusCode: StatusCodes.Status200OK) : Results.NotFound(); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/WorkflowInstances/List.cs b/src/api/Elsa.Api/Endpoints/WorkflowInstances/List.cs new file mode 100644 index 000000000..655ace87b --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowInstances/List.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Management.Serialization; +using Elsa.Mediator.Contracts; +using Elsa.Persistence.Entities; +using Elsa.Persistence.Requests; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.WorkflowInstances; + +public static partial class WorkflowInstances +{ + public static async Task ListAsync( + IRequestSender requestSender, + WorkflowSerializerOptionsProvider serializerOptionsProvider, + CancellationToken cancellationToken, + [FromQuery] int? page, + [FromQuery] int? pageSize, + [FromQuery] string? searchTerm, + [FromQuery] string? definitionId, + [FromQuery] string? correlationId, + [FromQuery] int? version, + [FromQuery] WorkflowStatus? workflowStatus, + [FromQuery] OrderBy? orderBy, + [FromQuery] OrderDirection? orderDirection) + { + var serializerOptions = serializerOptionsProvider.CreateSerializerOptions(); + var skip = page * pageSize; + var take = pageSize; + var request = new ListWorkflowInstanceSummaries(searchTerm, definitionId, version, correlationId, workflowStatus, orderBy ?? OrderBy.Created, orderDirection ?? OrderDirection.Ascending, skip ?? 0, take ?? 50); + var summaries = await requestSender.RequestAsync(request, cancellationToken); + + return Results.Json(summaries, serializerOptions, statusCode: StatusCodes.Status200OK); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/WorkflowInstances/Resource.cs b/src/api/Elsa.Api/Endpoints/WorkflowInstances/Resource.cs new file mode 100644 index 000000000..9faa62674 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowInstances/Resource.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Api.Endpoints.WorkflowInstances; + +public class WorkflowInstancesResource : ResourceBase +{ + public WorkflowInstancesResource(IEndpointRouteBuilder endpoints) : base(endpoints) + { + } + + public WorkflowInstancesResource List() => MapGet("api/workflow-instances", WorkflowInstances.ListAsync); + public WorkflowInstancesResource Get() => MapGet("api/workflow-instances/{id}", WorkflowInstances.GetAsync); +} + +public static class WorkflowsResourceExtensions +{ + public static IEndpointRouteBuilder MapWorkflowInstances(this IEndpointRouteBuilder endpoints, Action configureResource) + { + configureResource(new(endpoints)); + return endpoints; + } + + public static IEndpointRouteBuilder MapWorkflowInstances(this IEndpointRouteBuilder endpoints) => endpoints.MapWorkflowInstances(x => x + .List() + .Get() + ); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Workflows/Dispatch.cs b/src/api/Elsa.Api/Endpoints/Workflows/Dispatch.cs new file mode 100644 index 000000000..e1be129c9 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Workflows/Dispatch.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Api.ApiResults; +using Elsa.Persistence.Models; +using Elsa.Runtime.Contracts; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Endpoints.Workflows; + +public static partial class Workflows +{ + public static async Task DispatchAsync(string definitionId, IWorkflowRegistry workflowRegistry, HttpResponse response, CancellationToken cancellationToken) + { + var workflow = await workflowRegistry.FindByIdAsync(definitionId, VersionOptions.Published, cancellationToken); + return workflow == null ? Results.NotFound() : new DispatchWorkflowResult(workflow); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Workflows/Execute.cs b/src/api/Elsa.Api/Endpoints/Workflows/Execute.cs new file mode 100644 index 000000000..355566f20 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Workflows/Execute.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Api.ApiResults; +using Elsa.Persistence.Models; +using Elsa.Runtime.Contracts; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Endpoints.Workflows; + +public static partial class Workflows +{ + public static async Task ExecuteAsync(string definitionId, IWorkflowRegistry workflowRegistry, HttpResponse response, CancellationToken cancellationToken) + { + var workflow = await workflowRegistry.FindByIdAsync(definitionId, VersionOptions.Published, cancellationToken); + return workflow == null ? Results.NotFound() : new ExecuteWorkflowResult(workflow); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Workflows/Get.cs b/src/api/Elsa.Api/Endpoints/Workflows/Get.cs new file mode 100644 index 000000000..4b382021b --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Workflows/Get.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Management.Serialization; +using Elsa.Mediator.Contracts; +using Elsa.Persistence.Models; +using Elsa.Persistence.Requests; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.Workflows; + +public static partial class Workflows +{ + public static async Task Get( + IRequestSender requestSender, + WorkflowSerializerOptionsProvider serializerOptionsProvider, + CancellationToken cancellationToken, + [FromRoute] string definitionId, + [FromQuery] string? versionOptions = default) + { + var serializerOptions = serializerOptionsProvider.CreateSerializerOptions(); + var parsedVersionOptions = versionOptions != null ? VersionOptions.FromString(versionOptions) : VersionOptions.Latest; + var workflow = await requestSender.RequestAsync(new FindWorkflow(definitionId, parsedVersionOptions), cancellationToken); + return workflow == null ? Results.NotFound() : Results.Json(workflow, serializerOptions, statusCode: StatusCodes.Status200OK); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Workflows/GetMany.cs b/src/api/Elsa.Api/Endpoints/Workflows/GetMany.cs new file mode 100644 index 000000000..bdb141532 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Workflows/GetMany.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Management.Serialization; +using Elsa.Mediator.Contracts; +using Elsa.Persistence.Models; +using Elsa.Persistence.Requests; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.Workflows; + +public static partial class Workflows +{ + public static async Task GetManyAsync( + IRequestSender requestSender, + WorkflowSerializerOptionsProvider serializerOptionsProvider, + CancellationToken cancellationToken, + [FromQuery] string? definitionIds = default, + [FromQuery] string? versionOptions = default) + { + var serializerOptions = serializerOptionsProvider.CreateSerializerOptions(); + var splitIds = definitionIds?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + var parsedVersionOptions = versionOptions != null ? VersionOptions.FromString(versionOptions) : VersionOptions.Latest; + var workflowSummaries = await requestSender.RequestAsync(new FindManyWorkflowDefinitions(splitIds, parsedVersionOptions), cancellationToken); + + return Results.Json(workflowSummaries, serializerOptions, statusCode: StatusCodes.Status200OK); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Workflows/List.cs b/src/api/Elsa.Api/Endpoints/Workflows/List.cs new file mode 100644 index 000000000..01e0eb55e --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Workflows/List.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Management.Serialization; +using Elsa.Mediator.Contracts; +using Elsa.Persistence.Models; +using Elsa.Persistence.Requests; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.Workflows; + +public static partial class Workflows +{ + public static async Task ListAsync( + IRequestSender requestSender, + WorkflowSerializerOptionsProvider serializerOptionsProvider, + CancellationToken cancellationToken, + [FromQuery] string? versionOptions, + [FromQuery] int? page, + [FromQuery] int? pageSize) + { + var serializerOptions = serializerOptionsProvider.CreateSerializerOptions(); + var skip = page * pageSize; + var take = pageSize; + var parsedVersionOptions = versionOptions != null ? VersionOptions.FromString(versionOptions) : default; + var workflowSummaries = await requestSender.RequestAsync(new ListWorkflowSummaries(parsedVersionOptions, skip, take), cancellationToken); + + return Results.Json(workflowSummaries, serializerOptions, statusCode: StatusCodes.Status200OK); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Workflows/Post.cs b/src/api/Elsa.Api/Endpoints/Workflows/Post.cs new file mode 100644 index 000000000..31ff0c0cf --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Workflows/Post.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Workflows; +using Elsa.Contracts; +using Elsa.Management.Contracts; +using Elsa.Management.Serialization; +using Elsa.Persistence.Mappers; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Endpoints.Workflows; + +public static partial class Workflows +{ + public record SaveWorkflowRequest( + string? DefinitionId, + string? Name, + string? Description, + IActivity? Root, + ICollection? Triggers, + bool Publish); + + public static async Task PostAsync( + HttpContext httpContext, + WorkflowSerializerOptionsProvider serializerOptionsProvider, + IWorkflowPublisher workflowPublisher, + WorkflowDefinitionMapper mapper, + CancellationToken cancellationToken) + { + var serializerOptions = serializerOptionsProvider.CreateSerializerOptions(); + var model = (await httpContext.Request.ReadFromJsonAsync(serializerOptions, cancellationToken))!; + var definitionId = model.DefinitionId; + + // Get a workflow draft version. + var draft = !string.IsNullOrWhiteSpace(definitionId) + ? await workflowPublisher.GetDraftAsync(definitionId, cancellationToken) + : default; + + var isNew = draft == null; + + // Create a new workflow in case no existing definition was found. + if (draft == null) + { + draft = workflowPublisher.New(); + + if (!string.IsNullOrWhiteSpace(definitionId)) + draft = draft.WithDefinitionId(definitionId); + } + + // Update the draft with the received model. + var root = model.Root ?? new Sequence(); + var triggers = model.Triggers ?? new List(); + + draft = draft with + { + Root = root, + Triggers = triggers, + Metadata = draft.Metadata with + { + Name = model.Name, + Description = model.Description + } + }; + + draft = model.Publish ? await workflowPublisher.PublishAsync(draft, cancellationToken) : await workflowPublisher.SaveDraftAsync(draft, cancellationToken); + var statusCode = isNew ? StatusCodes.Status201Created : StatusCodes.Status200OK; + + return Results.Json(draft, serializerOptions, statusCode: statusCode); + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/Workflows/Resource.cs b/src/api/Elsa.Api/Endpoints/Workflows/Resource.cs new file mode 100644 index 000000000..7a69d60f4 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/Workflows/Resource.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Api.Endpoints.Workflows; + +public class WorkflowsResource : ResourceBase +{ + public WorkflowsResource(IEndpointRouteBuilder endpoints) : base(endpoints) + { + } + + public WorkflowsResource Post() => MapPost("api/workflows", Workflows.PostAsync); + public WorkflowsResource List() => MapGet("api/workflows", Workflows.ListAsync); + public WorkflowsResource GetMany() => MapGet("api/workflows/set", Workflows.GetManyAsync); + public WorkflowsResource Get() => MapGet("api/workflows/{definitionId}", Workflows.Get); + public WorkflowsResource Execute() => MapPost("api/workflows/{definitionId}/execute", Workflows.ExecuteAsync); + public WorkflowsResource Dispatch() => MapPost("api/workflows/{definitionId}/dispatch", Workflows.DispatchAsync); +} + +public static class WorkflowsResourceExtensions +{ + public static IEndpointRouteBuilder MapWorkflows(this IEndpointRouteBuilder endpoints, Action configureResource) + { + configureResource(new(endpoints)); + return endpoints; + } + + public static IEndpointRouteBuilder MapWorkflows(this IEndpointRouteBuilder endpoints) => endpoints.MapWorkflows(x => x + .Post() + .Execute() + .Dispatch() + .List() + .GetMany() + .Get() + ); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Extensions/ApplicationBuilderExtensions.cs b/src/api/Elsa.Api/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 000000000..3e1337603 --- /dev/null +++ b/src/api/Elsa.Api/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,9 @@ +using Elsa.Api.Middleware; +using Microsoft.AspNetCore.Builder; + +namespace Elsa.Api.Extensions; + +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder UseJsonSerializationErrorHandler(this IApplicationBuilder app) => app.UseMiddleware(); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Extensions/EndpointRouteBuilderExtensions.cs b/src/api/Elsa.Api/Extensions/EndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000..0a4385896 --- /dev/null +++ b/src/api/Elsa.Api/Extensions/EndpointRouteBuilderExtensions.cs @@ -0,0 +1,18 @@ +using Elsa.Api.Endpoints.ActivityDescriptors; +using Elsa.Api.Endpoints.Events; +using Elsa.Api.Endpoints.TriggerDescriptors; +using Elsa.Api.Endpoints.WorkflowInstances; +using Elsa.Api.Endpoints.Workflows; +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Api.Extensions; + +public static class EndpointRouteBuilderExtensions +{ + public static IEndpointRouteBuilder MapElsaApiEndpoints(this IEndpointRouteBuilder endpoints) => endpoints + .MapWorkflows() + .MapWorkflowInstances() + .MapActivityDescriptors() + .MapTriggerDescriptors() + .MapEvents(); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Middleware/JsonSerializationErrorHandlerMiddleware.cs b/src/api/Elsa.Api/Middleware/JsonSerializationErrorHandlerMiddleware.cs new file mode 100644 index 000000000..5f0eb9d54 --- /dev/null +++ b/src/api/Elsa.Api/Middleware/JsonSerializationErrorHandlerMiddleware.cs @@ -0,0 +1,43 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Api.Middleware; + +/// +/// Catches JSON serialization errors during POST requests and returns Bad Request responses. +/// +public class JsonSerializationErrorHandlerMiddleware +{ + private readonly RequestDelegate _next; + public JsonSerializationErrorHandlerMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext httpContext) + { + var method = httpContext.Request.Method; + + // If the HTTP verb is anything but POST or PUT, do nothing. + if (!string.Equals(method, "POST", StringComparison.InvariantCultureIgnoreCase) && !string.Equals(method, "PUT", StringComparison.InvariantCultureIgnoreCase)) + { + await _next(httpContext); + return; + } + + try + { + await _next(httpContext); + } + catch (JsonException e) + { + var model = new + { + ModelBindingError = e.Message + }; + + var result = Results.BadRequest(model); + + await result.ExecuteAsync(httpContext); + } + } +} \ No newline at end of file diff --git a/src/api/Elsa.Api/ResourceBase.cs b/src/api/Elsa.Api/ResourceBase.cs new file mode 100644 index 000000000..ef8a4c221 --- /dev/null +++ b/src/api/Elsa.Api/ResourceBase.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Api; + +public abstract class ResourceBase where T:ResourceBase +{ + protected ResourceBase(IEndpointRouteBuilder endpoints) => Endpoints = endpoints; + protected IEndpointRouteBuilder Endpoints { get; } + + protected T MapGet(string pattern, Delegate handler) + { + Endpoints.MapGet(pattern, handler); + return (T)this; + } + + protected T MapPost(string pattern, Delegate handler) + { + Endpoints.MapPost(pattern, handler); + return (T)this; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Console/ReadLine.cs b/src/core/Elsa.Core/Activities/Console/ReadLine.cs new file mode 100644 index 000000000..4ee322a6d --- /dev/null +++ b/src/core/Elsa.Core/Activities/Console/ReadLine.cs @@ -0,0 +1,21 @@ +using Elsa.Attributes; +using Elsa.Models; + +namespace Elsa.Activities.Console; + +public class ReadLine : Activity +{ + public ReadLine() + { + } + + public ReadLine(Variable variable, Func? valueConverter = default) => Output = new Output(variable, valueConverter); + + [Output] public Output? Output { get; set; } + + protected override void Execute(ActivityExecutionContext context) + { + var text = System.Console.ReadLine(); + context.Set(Output, text); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Console/WriteLine.cs b/src/core/Elsa.Core/Activities/Console/WriteLine.cs new file mode 100644 index 000000000..d41206188 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Console/WriteLine.cs @@ -0,0 +1,37 @@ +using System.ComponentModel; +using Elsa.Models; + +namespace Elsa.Activities.Console; + +public class WriteLine : Activity +{ + public WriteLine() + { + } + + public WriteLine(string text) : this(new Literal(text)) + { + } + + public WriteLine(Func text) : this(new DelegateReference(text)) + { + } + + public WriteLine(Func text) : this(new DelegateReference(text)) + { + } + + public WriteLine(Variable variable) => Text = new Input(variable); + public WriteLine(Literal literal) => Text = new Input(literal); + public WriteLine(DelegateReference delegateExpression) => Text = new Input(delegateExpression); + public WriteLine(Input text) => Text = text; + + [Description("The text to write.")] + public Input Text { get; set; } = default!; + + protected override void Execute(ActivityExecutionContext context) + { + var text = context.Get(Text); + System.Console.WriteLine(text); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/For.cs b/src/core/Elsa.Core/Activities/ControlFlow/For.cs new file mode 100644 index 000000000..1ced6704f --- /dev/null +++ b/src/core/Elsa.Core/Activities/ControlFlow/For.cs @@ -0,0 +1,70 @@ +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.ControlFlow; + +public enum ForOperator +{ + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual +} + +public class For : Activity +{ + public Input Start { get; set; } = new(0); + public Input End { get; set; } = new(0); + public Input Step { get; set; } = new(1); + public Input Operator { get; set; } = new(ForOperator.LessThanOrEqual); + [Outbound] public IActivity? Body { get; set; } + public Variable CurrentValue { get; set; } = new(); + + protected override void Execute(ActivityExecutionContext context) + { + var iterateNode = Body; + + if (iterateNode == null) + return; + + context.ExpressionExecutionContext.Register.Declare(CurrentValue); + HandleIteration(context); + } + + private void HandleIteration(ActivityExecutionContext context) + { + var iterateNode = Body!; + var end = context.Get(End); + var currentValue = CurrentValue.Get(context.ExpressionExecutionContext); + + // Initialize or increment. + var start = context.Get(Start); + var step = context.Get(Step); + var op = context.Get(Operator); + currentValue = currentValue == null ? start : currentValue + step; + + var loop = op switch + { + ForOperator.LessThan => currentValue < end, + ForOperator.LessThanOrEqual => currentValue <= end, + ForOperator.GreaterThan => currentValue > end, + ForOperator.GreaterThanOrEqual => currentValue >= end, + _ => throw new NotSupportedException() + }; + + if (loop) + { + context.ScheduleActivity(iterateNode, OnChildComplete); + + // Update loop variable. + CurrentValue.Set(context.ExpressionExecutionContext, currentValue); + } + } + + private ValueTask OnChildComplete(ActivityExecutionContext completedActivityExecutionContext, ActivityExecutionContext ownerActivityExecutionContext) + { + HandleIteration(ownerActivityExecutionContext); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs b/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs new file mode 100644 index 000000000..410eb5dfc --- /dev/null +++ b/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs @@ -0,0 +1,44 @@ +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.ControlFlow; + +public class ForEach : Activity +{ + [Input] public Input> Items { get; set; } = new(Array.Empty()); + [Outbound] public IActivity Body { get; set; } = default!; + public Variable CurrentValue { get; set; } = new(); + private const string CurrentIndexProperty = "CurrentIndex"; + + protected override void Execute(ActivityExecutionContext context) + { + // Declare looping variable. + context.ExpressionExecutionContext.Register.Declare(CurrentValue); + + // Execute first iteration. + HandleIteration(context); + } + + private void HandleIteration(ActivityExecutionContext context) + { + var currentIndex = context.GetProperty(CurrentIndexProperty); + var items = context.Get(Items)!.ToList(); + + if (currentIndex >= items.Count) + return; + + var currentItem = items[currentIndex]; + CurrentValue.Set(context, currentItem); + context.ScheduleActivity(Body, OnChildCompleted); + + // Increment index. + context.UpdateProperty(CurrentIndexProperty, x => x + 1); + } + + private ValueTask OnChildCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + HandleIteration(context); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs b/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs new file mode 100644 index 000000000..72f95eb04 --- /dev/null +++ b/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs @@ -0,0 +1,67 @@ +using System.Collections.Immutable; +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Extensions; +using Elsa.Models; + +namespace Elsa.Activities.ControlFlow; + +public class Fork : Activity +{ + [Input] public Input JoinMode { get; set; } = new(ControlFlow.JoinMode.WaitAny); + [Outbound] public ICollection Branches { get; set; } = new List(); + + protected override void Execute(ActivityExecutionContext context) => context.ScheduleActivities(Branches.Reverse(), CompleteChildAsync); + + private ValueTask CompleteChildAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + OnChildCompleted(context, childContext); + return ValueTask.CompletedTask; + } + + private void OnChildCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + var completedChildActivityId = childContext.Activity.Id; + + // Append activity to set of completed activities. + var completedActivityIds = context.UpdateProperty>("Completed", set => + { + set ??= new HashSet(); + set.Add(completedChildActivityId); + return set; + }); + + var allChildActivityIds = Branches.Select(x => x.Id).ToImmutableHashSet(); + var joinMode = context.Get(JoinMode); + + switch (joinMode) + { + case ControlFlow.JoinMode.WaitAny: + { + // Remove any and all bookmarks from other branches. + RemoveBookmarks(context); + } + break; + case ControlFlow.JoinMode.WaitAll: + { + var allSet = allChildActivityIds.All(x => completedActivityIds.Contains(x)); + + if (!allSet) + context.PreventContinuation(); + } + break; + } + } + + private void RemoveBookmarks(ActivityExecutionContext context) + { + // Find all descendants for each branch and remove any associated bookmarks. + var workflowExecutionContext = context.WorkflowExecutionContext; + var forkNode = context.ActivityNode; + var branchNodes = forkNode.Children; + var branchDescendantActivityIds = branchNodes.SelectMany(x => x.Flatten()).Select(x => x.Activity.Id).ToHashSet(); + var bookmarksToRemove = workflowExecutionContext.Bookmarks.Where(x => branchDescendantActivityIds.Contains(x.ActivityId)).ToList(); + + workflowExecutionContext.UnregisterBookmarks(bookmarksToRemove); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/If.cs b/src/core/Elsa.Core/Activities/ControlFlow/If.cs new file mode 100644 index 000000000..69beb05b1 --- /dev/null +++ b/src/core/Elsa.Core/Activities/ControlFlow/If.cs @@ -0,0 +1,26 @@ +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.ControlFlow; + +public class If : Activity +{ + [Input] public Input Condition { get; set; } = new(new Literal(false)); + [Outbound] public IActivity? Then { get; set; } + [Outbound] public IActivity? Else { get; set; } + + protected override void Execute(ActivityExecutionContext context) + { + var result = context.Get(Condition); + var nextNode = result ? Then : Else; + + if (nextNode != null) + context.ScheduleActivity(nextNode, OnChildCompletedAsync); + } + + private ValueTask OnChildCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Join.cs b/src/core/Elsa.Core/Activities/ControlFlow/Join.cs new file mode 100644 index 000000000..9d6b92c7f --- /dev/null +++ b/src/core/Elsa.Core/Activities/ControlFlow/Join.cs @@ -0,0 +1,85 @@ +using Elsa.Attributes; +using Elsa.Models; + +namespace Elsa.Activities.ControlFlow; + +public enum JoinMode +{ + WaitAny, + WaitAll +} + +public class Join : Activity +{ + public Join(JoinMode joinMode = ControlFlow.JoinMode.WaitAny) + { + JoinMode = new Input(joinMode); + } + + [Input] public Input JoinMode { get; set; } + + protected override void Execute(ActivityExecutionContext context) + { + var joinMode = context.Get(JoinMode); + + switch (joinMode) + { + case ControlFlow.JoinMode.WaitAny: + { + // Remove any and all bookmarks from other branches. + RemoveBookmarks(context); + } + break; + case ControlFlow.JoinMode.WaitAll: + { + var workflowExecutionContext = context.WorkflowExecutionContext; + + // Record last executed activity ID. + var lastActivityInstanceId = context.WorkflowExecutionContext.ExecutionLog.LastOrDefault()?.ActivityInstanceId; + var lastActivityId = lastActivityInstanceId != null ? workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == lastActivityInstanceId)?.Activity.Id : default; + + if (lastActivityId != null) + { + var recordedActivitiesKey = $"{Id}:RecordedActivities"; + var recordedActivityIds = workflowExecutionContext.UpdateProperty>(recordedActivitiesKey, set => + { + set ??= new HashSet(); + set.Add(lastActivityId); + return set; + }); + + var inboundActivityIds = context.ActivityNode.Parents.Select(x => x.Activity.Id).ToHashSet(); + var allSet = inboundActivityIds.All(x => recordedActivityIds.Contains(x)); + + if (!allSet) + { + // If not all inbound activities have executed yet, instruct engine to not complete this activity. + context.PreventContinuation(); + } + else + { + // If all inbound activities have executed, clean global state. + workflowExecutionContext.Properties.Remove(recordedActivitiesKey); + } + } + + break; + } + } + } + + private void RemoveBookmarks(ActivityExecutionContext context) + { + var workflowExecutionContext = context.WorkflowExecutionContext; + + // Remove all bookmarks of ancestors and siblings. + var joinNode = context.ActivityNode; + var ancestors = joinNode.Ancestors().Select(x => x.Activity.Id); + var siblingsAndCousins = joinNode.SiblingsAndCousins().Select(x => x.Activity.Id); + var activitiesInPath = ancestors.Concat(siblingsAndCousins).ToHashSet(); + var bookmarksToRemove = workflowExecutionContext.Bookmarks.Where(x => activitiesInPath.Contains(x.ActivityId)).ToList(); + + // Remove any bookmarks of any ancestors. + workflowExecutionContext.UnregisterBookmarks(bookmarksToRemove); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/ParallelForEach.cs b/src/core/Elsa.Core/Activities/ControlFlow/ParallelForEach.cs new file mode 100644 index 000000000..0bcf4e78c --- /dev/null +++ b/src/core/Elsa.Core/Activities/ControlFlow/ParallelForEach.cs @@ -0,0 +1,44 @@ +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.ControlFlow; + +public class ParallelForEach : Activity +{ + private const string CollectedCountProperty = nameof(CollectedCountProperty); + [Input] public Input> Items { get; set; } = new(Array.Empty()); + [Outbound] public IActivity Body { get; set; } = default!; + public Variable CurrentValue { get; set; } = new(); + + protected override void Execute(ActivityExecutionContext context) + { + // Declare looping variable. + context.ExpressionExecutionContext.Register.Declare(CurrentValue); + var items = context.Get(Items)!.Reverse().ToList(); + + foreach (var item in items) + { + var localVariable = new Variable(item) + { + // "Capture" the CurrentValues variable by use same ID so that outer scope variable can access inner scope variable. + Id = CurrentValue.Id + }; + + // Schedule a body of work for each item. + context.ScheduleActivity(Body, OnChildCompleted, new[]{localVariable}); + } + } + + private ValueTask OnChildCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + var itemCount = context.Get(Items)!.Count; + var collectedCount = context.UpdateProperty(CollectedCountProperty, count => count + 1); + + // Prevent next sibling from executing while not all scheduled activities have completed. + if(collectedCount < itemCount) + context.PreventContinuation(); + + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/While.cs b/src/core/Elsa.Core/Activities/ControlFlow/While.cs new file mode 100644 index 000000000..8e19fa3fc --- /dev/null +++ b/src/core/Elsa.Core/Activities/ControlFlow/While.cs @@ -0,0 +1,27 @@ +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.ControlFlow; + +public class While : Activity +{ + [Input] public Input Condition { get; set; } = new(false); + [Outbound] public IActivity Body { get; set; } = default!; + + protected override void Execute(ActivityExecutionContext context) + { + var loop = context.Get(Condition); + + if(loop) + context.ScheduleActivity(Body, OnBodyCompleted); + } + + private async ValueTask OnBodyCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + var loop = await context.EvaluateAsync(Condition); + + if(loop) + context.ScheduleActivity(Body, OnBodyCompleted); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/Event.cs b/src/core/Elsa.Core/Activities/Primitives/Event.cs new file mode 100644 index 000000000..187cdd953 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Primitives/Event.cs @@ -0,0 +1,40 @@ +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.Primitives; + +public class Event : Activity +{ + public Event() + { + } + + public Event(string eventName) : this(new Literal(eventName)) + { + } + + public Event(Func text) : this(new DelegateReference(text)) + { + } + + public Event(Func text) : this(new DelegateReference(text)) + { + } + + public Event(Variable variable) => EventName = new Input(variable); + public Event(Literal literal) => EventName = new Input(literal); + public Event(DelegateReference delegateExpression) => EventName = new Input(delegateExpression); + public Event(Input eventName) => EventName = eventName; + + [Input] public Input EventName { get; set; } = default!; + [Output] public Output? Payload { get; set; } + + protected override void Execute(ActivityExecutionContext context) + { + var hasher = context.GetRequiredService(); + var eventName = context.Get(EventName)!; + var hash = hasher.Hash(eventName); + context.SetBookmark(hash); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/UpdateVariable.cs b/src/core/Elsa.Core/Activities/Primitives/UpdateVariable.cs new file mode 100644 index 000000000..b469205b9 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Primitives/UpdateVariable.cs @@ -0,0 +1,16 @@ +using Elsa.Attributes; +using Elsa.Models; + +namespace Elsa.Activities.Primitives; + +public class UpdateVariable : Activity +{ + [Input] public Input Value { get; set; } = new Input(new Literal()); + public Variable Variable { get; set; } = default!; + + protected override void Execute(ActivityExecutionContext context) + { + var value = context.Get(Value); + Variable.Set(context, value); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Workflows/Connection.cs b/src/core/Elsa.Core/Activities/Workflows/Connection.cs new file mode 100644 index 000000000..805a70e46 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Workflows/Connection.cs @@ -0,0 +1,5 @@ +using Elsa.Contracts; + +namespace Elsa.Activities.Workflows; + +public record Connection(IActivity Source, IActivity Target, string? SourcePort, string TargetPort); \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Workflows/Container.cs b/src/core/Elsa.Core/Activities/Workflows/Container.cs new file mode 100644 index 000000000..d1bd27fd0 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Workflows/Container.cs @@ -0,0 +1,28 @@ +using System.Collections.ObjectModel; +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.Workflows; + +public abstract class Container : Activity, IContainer +{ + protected Container() + { + } + + protected Container(params IActivity[] activities) => Activities = activities; + [Outbound] public ICollection Activities { get; set; } = new List(); + public ICollection Variables { get; set; } = new Collection(); + + protected override void Execute(ActivityExecutionContext context) + { + // Register variables. + context.ExpressionExecutionContext.Register.Declare(Variables); + + // Schedule children. + ScheduleChildren(context); + } + + protected abstract void ScheduleChildren(ActivityExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Workflows/Flowchart.cs b/src/core/Elsa.Core/Activities/Workflows/Flowchart.cs new file mode 100644 index 000000000..dd65da4d5 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Workflows/Flowchart.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.Workflows; + +[Category("Workflows")] +public class Flowchart : Container +{ + [Node] public IActivity? Start { get; set; } = default!; + public ICollection Connections { get; set; } = new List(); + + protected override void ScheduleChildren(ActivityExecutionContext context) + { + if (Start == null!) + return; + + context.ScheduleActivity(Start, OnChildCompleted); + } + + private ValueTask OnChildCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + ScheduleChildren(context, childContext.Activity); + return ValueTask.CompletedTask; + } + + private void ScheduleChildren(ActivityExecutionContext context, IActivity parent) + { + if (parent == null!) + return; + + var outboundConnections = Connections.Where(x => x.Source == parent).ToList(); + var children = outboundConnections.Select(x => x.Target).ToList(); + + context.ScheduleActivities(children, OnChildCompleted); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Workflows/Sequence.cs b/src/core/Elsa.Core/Activities/Workflows/Sequence.cs new file mode 100644 index 000000000..1ef5dc161 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Workflows/Sequence.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Activities.Workflows; + +[Category("Workflows")] +public class Sequence : Container +{ + private const string CurrentIndexProperty = "CurrentIndex"; + + public Sequence() + { + } + + public Sequence(params IActivity[] activities) : base(activities) + { + } + + protected override void ScheduleChildren(ActivityExecutionContext context) + { + HandleItem(context); + } + + private void HandleItem(ActivityExecutionContext context) + { + var currentIndex = context.GetProperty(CurrentIndexProperty); + var childActivities = Activities.ToList(); + + if (currentIndex >= childActivities.Count) + return; + + var nextActivity = childActivities.ElementAt(currentIndex); + context.ScheduleActivity(nextActivity, OnChildCompleted); + context.UpdateProperty(CurrentIndexProperty, x => x + 1); + } + + private ValueTask OnChildCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + HandleItem(context); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/ActivityNodeResolvers/OutboundActivityNodeResolver.cs b/src/core/Elsa.Core/ActivityNodeResolvers/OutboundActivityNodeResolver.cs new file mode 100644 index 000000000..98513de5c --- /dev/null +++ b/src/core/Elsa.Core/ActivityNodeResolvers/OutboundActivityNodeResolver.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.ActivityNodeResolvers; + +public class OutboundActivityNodeResolver : IActivityNodeResolver +{ + public int Priority => -1; + public bool GetSupportsActivity(IActivity activity) => activity is Activity; + + public IEnumerable GetPorts(IActivity activity) => + GetSinglePorts(activity) + .Where(x => x != null) + .Select(x => x!) + .ToHashSet(); + + private static IEnumerable GetSinglePorts(IActivity activity) + { + var nodeType = activity.GetType(); + + var ports = + from prop in nodeType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + where typeof(IActivity).IsAssignableFrom(prop.PropertyType) || typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) + let portAttr = prop.GetCustomAttribute() + let nodeAttr = prop.GetCustomAttribute() + where portAttr != null || nodeAttr != null + let value = prop.GetValue(activity) + let isCollection = GetPropertyIsCollection(prop.PropertyType) + select isCollection ? (IEnumerable)value : new[] { (IActivity)value }; + + return ports.SelectMany(x => x); + } + + private static bool GetPropertyIsCollection(Type propertyType) => typeof(IEnumerable).IsAssignableFrom(propertyType); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/AssemblyInfo.cs b/src/core/Elsa.Core/AssemblyInfo.cs new file mode 100644 index 000000000..95465252a --- /dev/null +++ b/src/core/Elsa.Core/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: CLSCompliant(true)] \ No newline at end of file diff --git a/src/core/Elsa.Core/Attributes/ActivityAttribute.cs b/src/core/Elsa.Core/Attributes/ActivityAttribute.cs new file mode 100644 index 000000000..379069559 --- /dev/null +++ b/src/core/Elsa.Core/Attributes/ActivityAttribute.cs @@ -0,0 +1,12 @@ +namespace Elsa.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public class ActivityAttribute : Attribute +{ + public ActivityAttribute(string? typeName = default) + { + TypeName = typeName; + } + + public string? TypeName { get; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Attributes/InboundAttribute.cs b/src/core/Elsa.Core/Attributes/InboundAttribute.cs new file mode 100644 index 000000000..493b9aa2b --- /dev/null +++ b/src/core/Elsa.Core/Attributes/InboundAttribute.cs @@ -0,0 +1,6 @@ +namespace Elsa.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class InboundAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Attributes/InputAttribute.cs b/src/core/Elsa.Core/Attributes/InputAttribute.cs new file mode 100644 index 000000000..7acc9e7ba --- /dev/null +++ b/src/core/Elsa.Core/Attributes/InputAttribute.cs @@ -0,0 +1,72 @@ +namespace Elsa.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class InputAttribute : Attribute +{ + public string? Name { get; set; } + + /// + /// A hint to workflow tooling what input control to use. + /// + public string? UIHint { get; set; } + + /// + /// The user-friendly name of the activity property. + /// + public string? DisplayName { get; set; } + + // /// + // /// A brief description about this property for workflow tooling to use when displaying activity editors. + // /// + // public string? Description { get; set; } + + /// + /// A category to group this property with. + /// + public string? Category { get; set; } + + /// + /// A value representing options specific to a given UI hint. + /// + public object? Options { get; set; } + + /// + /// The type that provides options. If specified, this overrules any value specified via . + /// + public Type? OptionsProvider { get; set; } + + /// + /// A value to order this property by. Properties are displayed in ascending order (lower appears before higher). + /// + public float Order { get; set; } + + /// + /// The default value to set. + /// + public object? DefaultValue { get; set; } + + /// + /// The type that provides a default value. When specified, the will be ignored. + /// + public Type? DefaultValueProvider { get; set; } + + /// + /// The syntax to use by default when evaluating the value. Only used when the property definition doesn't have a syntax specified. + /// + public string? DefaultSyntax { get; set; } + + /// + /// The syntax to use by default when evaluating the value. Only used when the property definition doesn't have a syntax specified. + /// + public string[]? SupportedSyntaxes { get; set; } + + /// + /// A value indicating whether this property should be displayed but as read-only. + /// + public bool IsReadOnly { get; set; } + + /// + /// A value indicating whether this property should be visible. + /// + public bool IsBrowsable { get; set; } = true; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Attributes/NodeAttribute.cs b/src/core/Elsa.Core/Attributes/NodeAttribute.cs new file mode 100644 index 000000000..0e8eb0760 --- /dev/null +++ b/src/core/Elsa.Core/Attributes/NodeAttribute.cs @@ -0,0 +1,6 @@ +namespace Elsa.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class NodeAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Attributes/OutboundAttribute.cs b/src/core/Elsa.Core/Attributes/OutboundAttribute.cs new file mode 100644 index 000000000..ebd2fa275 --- /dev/null +++ b/src/core/Elsa.Core/Attributes/OutboundAttribute.cs @@ -0,0 +1,8 @@ +namespace Elsa.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class OutboundAttribute : Attribute +{ + public string? Name { get; set; } + public string? DisplayName { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Attributes/OutputAttribute.cs b/src/core/Elsa.Core/Attributes/OutputAttribute.cs new file mode 100644 index 000000000..26a18a142 --- /dev/null +++ b/src/core/Elsa.Core/Attributes/OutputAttribute.cs @@ -0,0 +1,15 @@ +namespace Elsa.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class OutputAttribute : Attribute +{ + /// + /// The technical name of the activity property. + /// + public string? Name { get; set; } + + /// + /// A brief description about this property for workflow tooling to use when displaying activity editors. + /// + public string? Description { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Attributes/StateAttribute.cs b/src/core/Elsa.Core/Attributes/StateAttribute.cs new file mode 100644 index 000000000..4ae20af2f --- /dev/null +++ b/src/core/Elsa.Core/Attributes/StateAttribute.cs @@ -0,0 +1,6 @@ +namespace Elsa.Attributes; + +[AttributeUsage(AttributeTargets.Property)] +public class StateAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Builders/WorkflowDefinitionBuilder.cs b/src/core/Elsa.Core/Builders/WorkflowDefinitionBuilder.cs new file mode 100644 index 000000000..db51e63fc --- /dev/null +++ b/src/core/Elsa.Core/Builders/WorkflowDefinitionBuilder.cs @@ -0,0 +1,55 @@ +using Elsa.Activities.Workflows; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Builders; + +public class WorkflowDefinitionBuilder : IWorkflowDefinitionBuilder +{ + public string? Id { get; private set; } + public string? DefinitionId { get; private set; } + public int Version { get; private set; } = 1; + public IActivity? Root { get; private set; } + public ICollection Triggers { get; } = new List(); + + public IWorkflowDefinitionBuilder WithId(string id) + { + Id = id; + return this; + } + + public IWorkflowDefinitionBuilder WithDefinitionId(string definitionId) + { + DefinitionId = definitionId; + return this; + } + + public IWorkflowDefinitionBuilder WithVersion(int version) + { + Version = version; + return this; + } + + public IWorkflowDefinitionBuilder WithRoot(IActivity root) + { + Root = root; + return this; + } + + public IWorkflowDefinitionBuilder AddTrigger(ITrigger trigger) + { + Triggers.Add(trigger); + return this; + } + + public Workflow BuildWorkflow() + { + var definitionId = DefinitionId ?? Guid.NewGuid().ToString("N"); + var id = Id ?? Guid.NewGuid().ToString("N"); + var root = Root ?? new Sequence(); + var identity = new WorkflowIdentity(definitionId, Version, id); + var publication = WorkflowPublication.LatestAndPublished; + var metadata = new WorkflowMetadata(); + return new Workflow(identity, publication, metadata, root, Triggers); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/Delegates.cs b/src/core/Elsa.Core/Contracts/Delegates.cs new file mode 100644 index 000000000..6ad9e4fe1 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/Delegates.cs @@ -0,0 +1,6 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public delegate ValueTask ExecuteActivityDelegate(ActivityExecutionContext context); +public delegate ValueTask ActivityCompletionCallback(ActivityExecutionContext context, ActivityExecutionContext childContext); \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivity.cs b/src/core/Elsa.Core/Contracts/IActivity.cs new file mode 100644 index 000000000..a103015fd --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivity.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IActivity : INode +{ + ValueTask ExecuteAsync(ActivityExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivityExecutionBuilder.cs b/src/core/Elsa.Core/Contracts/IActivityExecutionBuilder.cs new file mode 100644 index 000000000..eb8c6c90d --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivityExecutionBuilder.cs @@ -0,0 +1,10 @@ +using Elsa.Pipelines.ActivityExecution; + +namespace Elsa.Contracts; + +public interface IActivityExecutionBuilder +{ + IServiceProvider ServiceProvider { get; } + IActivityExecutionBuilder Use(Func middleware); + public ActivityMiddlewareDelegate Build(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivityExecutionMiddleware.cs b/src/core/Elsa.Core/Contracts/IActivityExecutionMiddleware.cs new file mode 100644 index 000000000..ea3250730 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivityExecutionMiddleware.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IActivityExecutionMiddleware +{ + ValueTask InvokeAsync(ActivityExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivityExecutionPipeline.cs b/src/core/Elsa.Core/Contracts/IActivityExecutionPipeline.cs new file mode 100644 index 000000000..a3fca6697 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivityExecutionPipeline.cs @@ -0,0 +1,11 @@ +using Elsa.Models; +using Elsa.Pipelines.ActivityExecution; + +namespace Elsa.Contracts; + +public interface IActivityExecutionPipeline +{ + ActivityMiddlewareDelegate Setup(Action setup); + ActivityMiddlewareDelegate Pipeline { get; } + Task ExecuteAsync(ActivityExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivityInvoker.cs b/src/core/Elsa.Core/Contracts/IActivityInvoker.cs new file mode 100644 index 000000000..64ef38013 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivityInvoker.cs @@ -0,0 +1,14 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IActivityInvoker +{ + Task InvokeAsync( + WorkflowExecutionContext workflowExecutionContext, + IActivity activity, + ActivityExecutionContext? owner = default, + IEnumerable? locationReferences = default); + + Task InvokeAsync(ActivityExecutionContext activityExecutionContext); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivityNodeResolver.cs b/src/core/Elsa.Core/Contracts/IActivityNodeResolver.cs new file mode 100644 index 000000000..2a87ae9cd --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivityNodeResolver.cs @@ -0,0 +1,8 @@ +namespace Elsa.Contracts; + +public interface IActivityNodeResolver +{ + int Priority { get; } + bool GetSupportsActivity(IActivity activity); + IEnumerable GetPorts(IActivity activity); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivityScheduler.cs b/src/core/Elsa.Core/Contracts/IActivityScheduler.cs new file mode 100644 index 000000000..3584d50a5 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivityScheduler.cs @@ -0,0 +1,11 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IActivityScheduler +{ + bool HasAny { get; } + void Push(ActivityWorkItem workItem); + ActivityWorkItem Pop(); + IEnumerable List(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivitySchedulerFactory.cs b/src/core/Elsa.Core/Contracts/IActivitySchedulerFactory.cs new file mode 100644 index 000000000..25fc9af59 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivitySchedulerFactory.cs @@ -0,0 +1,6 @@ +namespace Elsa.Contracts; + +public interface IActivitySchedulerFactory +{ + IActivityScheduler CreateScheduler(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IActivityWalker.cs b/src/core/Elsa.Core/Contracts/IActivityWalker.cs new file mode 100644 index 000000000..ccaec36e3 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IActivityWalker.cs @@ -0,0 +1,11 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +/// +/// Walks an activity tree starting at the root. +/// +public interface IActivityWalker +{ + ActivityNode Walk(IActivity activity); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IContainer.cs b/src/core/Elsa.Core/Contracts/IContainer.cs new file mode 100644 index 000000000..740022970 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IContainer.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IContainer : IActivity +{ + ICollection Variables { get; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IExpression.cs b/src/core/Elsa.Core/Contracts/IExpression.cs new file mode 100644 index 000000000..a07160828 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IExpression.cs @@ -0,0 +1,5 @@ +namespace Elsa.Contracts; + +public interface IExpression +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IExpressionEvaluator.cs b/src/core/Elsa.Core/Contracts/IExpressionEvaluator.cs new file mode 100644 index 000000000..231e584cc --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IExpressionEvaluator.cs @@ -0,0 +1,15 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IExpressionEvaluator +{ + ValueTask EvaluateAsync(IExpression input, ExpressionExecutionContext context); + ValueTask EvaluateAsync(IExpression input, Type returnType, ExpressionExecutionContext context); +} + +public static class ExpressionEvaluatorExtensions +{ + public static ValueTask EvaluateAsync(this IExpressionEvaluator evaluator, Input input, ExpressionExecutionContext context) => evaluator.EvaluateAsync(input.Expression, context); + public static ValueTask EvaluateAsync(this IExpressionEvaluator evaluator, Input input, ExpressionExecutionContext context) => evaluator.EvaluateAsync(input.Expression, input.Type, context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IExpressionHandler.cs b/src/core/Elsa.Core/Contracts/IExpressionHandler.cs new file mode 100644 index 000000000..f2a7f0740 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IExpressionHandler.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IExpressionHandler +{ + ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IExpressionHandlerRegistry.cs b/src/core/Elsa.Core/Contracts/IExpressionHandlerRegistry.cs new file mode 100644 index 000000000..d489e56d3 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IExpressionHandlerRegistry.cs @@ -0,0 +1,7 @@ +namespace Elsa.Contracts; + +public interface IExpressionHandlerRegistry +{ + void Register(Type expression, Type handler); + IExpressionHandler? GetHandler(IExpression input); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IHasher.cs b/src/core/Elsa.Core/Contracts/IHasher.cs new file mode 100644 index 000000000..79c2a3432 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IHasher.cs @@ -0,0 +1,9 @@ +namespace Elsa.Contracts; + +/// +/// Computes a hash for a given value. +/// +public interface IHasher +{ + public string Hash(object value); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IIdentityGenerator.cs b/src/core/Elsa.Core/Contracts/IIdentityGenerator.cs new file mode 100644 index 000000000..1dac3d46c --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IIdentityGenerator.cs @@ -0,0 +1,6 @@ +namespace Elsa.Contracts; + +public interface IIdentityGenerator +{ + string GenerateId(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IIdentityGraphService.cs b/src/core/Elsa.Core/Contracts/IIdentityGraphService.cs new file mode 100644 index 000000000..7d284207f --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IIdentityGraphService.cs @@ -0,0 +1,10 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IIdentityGraphService +{ + void AssignIdentities(Workflow workflow); + void AssignIdentities(IActivity root); + void AssignIdentities(ActivityNode root); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/INode.cs b/src/core/Elsa.Core/Contracts/INode.cs new file mode 100644 index 000000000..b4ee1fc25 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/INode.cs @@ -0,0 +1,8 @@ +namespace Elsa.Contracts; + +public interface INode +{ + string Id { get; set; } + string NodeType { get; set; } + IDictionary Metadata { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IReference.cs b/src/core/Elsa.Core/Contracts/IReference.cs new file mode 100644 index 000000000..8859212d6 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IReference.cs @@ -0,0 +1,5 @@ +namespace Elsa.Contracts; + +public interface IReference +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IScheduledNodeExecuted.cs b/src/core/Elsa.Core/Contracts/IScheduledNodeExecuted.cs new file mode 100644 index 000000000..4bf87fc86 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IScheduledNodeExecuted.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IScheduledNodeExecuted +{ + ValueTask HandleAsync(ActivityExecutionContext context, IActivity owner); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IScopeCompletedHandler.cs b/src/core/Elsa.Core/Contracts/IScopeCompletedHandler.cs new file mode 100644 index 000000000..06e450159 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IScopeCompletedHandler.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IScopeCompletedHandler +{ + ValueTask HandleAsync(ActivityExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/ISystemClock.cs b/src/core/Elsa.Core/Contracts/ISystemClock.cs new file mode 100644 index 000000000..654003bae --- /dev/null +++ b/src/core/Elsa.Core/Contracts/ISystemClock.cs @@ -0,0 +1,6 @@ +namespace Elsa.Contracts; + +public interface ISystemClock +{ + DateTime UtcNow { get; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/ITrigger.cs b/src/core/Elsa.Core/Contracts/ITrigger.cs new file mode 100644 index 000000000..60c5afe65 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/ITrigger.cs @@ -0,0 +1,9 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface ITrigger : INode +{ + string TriggerType { get; set; } + ValueTask> GetHashInputsAsync(TriggerIndexingContext context, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IWorkflowDefinitionBuilder.cs b/src/core/Elsa.Core/Contracts/IWorkflowDefinitionBuilder.cs new file mode 100644 index 000000000..f17424768 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IWorkflowDefinitionBuilder.cs @@ -0,0 +1,16 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IWorkflowDefinitionBuilder +{ + string? DefinitionId { get; } + int Version { get; } + IActivity? Root { get; } + ICollection Triggers { get; } + IWorkflowDefinitionBuilder WithDefinitionId(string definitionId); + IWorkflowDefinitionBuilder WithVersion(int version); + IWorkflowDefinitionBuilder WithRoot(IActivity root); + IWorkflowDefinitionBuilder AddTrigger(ITrigger trigger); + Workflow BuildWorkflow(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IWorkflowEngine.cs b/src/core/Elsa.Core/Contracts/IWorkflowEngine.cs new file mode 100644 index 000000000..62dfc5c75 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IWorkflowEngine.cs @@ -0,0 +1,20 @@ +using Elsa.Models; +using Elsa.State; + +namespace Elsa.Contracts; + +public interface IWorkflowEngine +{ + Task ExecuteAsync(Workflow workflow, CancellationToken cancellationToken = default); + Task ExecuteAsync(Workflow workflow, WorkflowState workflowState, CancellationToken cancellationToken = default); + Task ExecuteAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmark, CancellationToken cancellationToken = default); + Task ExecuteAsync(WorkflowExecutionContext workflowExecutionContext); + + WorkflowExecutionContext CreateWorkflowExecutionContext( + IServiceProvider serviceProvider, + Workflow workflow, + WorkflowState? workflowState = default, + Bookmark? bookmark = default, + ExecuteActivityDelegate? executeActivityDelegate = default, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IWorkflowExecutionBuilder.cs b/src/core/Elsa.Core/Contracts/IWorkflowExecutionBuilder.cs new file mode 100644 index 000000000..491fb8cc5 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IWorkflowExecutionBuilder.cs @@ -0,0 +1,11 @@ +using Elsa.Pipelines.WorkflowExecution; + +namespace Elsa.Contracts; + +public interface IWorkflowExecutionBuilder +{ + public IDictionary Properties { get; } + IServiceProvider ApplicationServices { get; } + IWorkflowExecutionBuilder Use(Func middleware); + public WorkflowMiddlewareDelegate Build(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IWorkflowExecutionMiddleware.cs b/src/core/Elsa.Core/Contracts/IWorkflowExecutionMiddleware.cs new file mode 100644 index 000000000..f83acffdf --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IWorkflowExecutionMiddleware.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Contracts; + +public interface IWorkflowExecutionMiddleware +{ + ValueTask InvokeAsync(WorkflowExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IWorkflowExecutionPipeline.cs b/src/core/Elsa.Core/Contracts/IWorkflowExecutionPipeline.cs new file mode 100644 index 000000000..81f3ca011 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IWorkflowExecutionPipeline.cs @@ -0,0 +1,11 @@ +using Elsa.Models; +using Elsa.Pipelines.WorkflowExecution; + +namespace Elsa.Contracts; + +public interface IWorkflowExecutionPipeline +{ + WorkflowMiddlewareDelegate Setup(Action setup); + WorkflowMiddlewareDelegate Pipeline { get; } + Task ExecuteAsync(WorkflowExecutionContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Contracts/IWorkflowStateSerializer.cs b/src/core/Elsa.Core/Contracts/IWorkflowStateSerializer.cs new file mode 100644 index 000000000..38dc45c20 --- /dev/null +++ b/src/core/Elsa.Core/Contracts/IWorkflowStateSerializer.cs @@ -0,0 +1,10 @@ +using Elsa.Models; +using Elsa.State; + +namespace Elsa.Contracts; + +public interface IWorkflowStateSerializer +{ + WorkflowState ReadState(WorkflowExecutionContext workflowExecutionContext); + void WriteState(WorkflowExecutionContext workflowExecutionContext, WorkflowState state); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Elsa.Core.csproj b/src/core/Elsa.Core/Elsa.Core.csproj new file mode 100644 index 000000000..c7a8c3989 --- /dev/null +++ b/src/core/Elsa.Core/Elsa.Core.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + Elsa + latest + + + + + + + + + + + + + + diff --git a/src/core/Elsa.Core/Elsa.Core.csproj.DotSettings b/src/core/Elsa.Core/Elsa.Core.csproj.DotSettings new file mode 100644 index 000000000..373203e26 --- /dev/null +++ b/src/core/Elsa.Core/Elsa.Core.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/src/core/Elsa.Core/Exceptions/TypeConversionException.cs b/src/core/Elsa.Core/Exceptions/TypeConversionException.cs new file mode 100644 index 000000000..bbfd5c982 --- /dev/null +++ b/src/core/Elsa.Core/Exceptions/TypeConversionException.cs @@ -0,0 +1,19 @@ +namespace Elsa.Exceptions; + +public class TypeConversionException : Exception +{ + public object Value { get; } + public Type TargetType { get; } + + public TypeConversionException(string message, object value, Type targetType) : base(message) + { + Value = value; + TargetType = targetType; + } + + public TypeConversionException(string message, object value, Type targetType, Exception innerException) : base(message, innerException) + { + Value = value; + TargetType = targetType; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Expressions/DelegateExpression.cs b/src/core/Elsa.Core/Expressions/DelegateExpression.cs new file mode 100644 index 000000000..328dbe5f2 --- /dev/null +++ b/src/core/Elsa.Core/Expressions/DelegateExpression.cs @@ -0,0 +1,32 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Expressions; + +public class DelegateExpression : IExpression +{ + public DelegateExpression(DelegateReference delegateReference) + { + DelegateReference = delegateReference; + } + + public DelegateReference DelegateReference { get; } +} + +public class DelegateExpression : DelegateExpression +{ + public DelegateExpression(DelegateReference delegateReference) : base(delegateReference) + { + } +} + +public class DelegateExpressionHandler : IExpressionHandler +{ + public async ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) + { + var delegateExpression = (DelegateExpression)expression; + var @delegate = delegateExpression.DelegateReference.Delegate; + var value = @delegate != null ? await @delegate(context) : default; + return value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Expressions/ElsaExpression.cs b/src/core/Elsa.Core/Expressions/ElsaExpression.cs new file mode 100644 index 000000000..97c32d5cd --- /dev/null +++ b/src/core/Elsa.Core/Expressions/ElsaExpression.cs @@ -0,0 +1,25 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Expressions; + +public class ElsaExpression : IExpression +{ + public ElsaExpression(string scriptExpression) + { + ScriptExpression = scriptExpression; + } + + public string ScriptExpression { get; } +} + +public class ElsaExpressionHandler : IExpressionHandler +{ + public ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) + { + var elsaExpression = (ElsaExpression)expression; + + // TODO: evaluate expression. + return new ValueTask(elsaExpression.ScriptExpression); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Expressions/JsonExpression.cs b/src/core/Elsa.Core/Expressions/JsonExpression.cs new file mode 100644 index 000000000..7397d7051 --- /dev/null +++ b/src/core/Elsa.Core/Expressions/JsonExpression.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Expressions; + +public class JsonExpression : IExpression +{ + public JsonExpression(string? value) => Value = value; + public string? Value { get; } +} + +public class JsonExpression : LiteralExpression +{ + public JsonExpression(T? value) : base(value) + { + } +} + +public class JsonExpressionHandler : IExpressionHandler +{ + public ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) + { + var jsonExpression = (JsonExpression)expression; + var value = jsonExpression.Value; + + if (string.IsNullOrWhiteSpace(value)) + return ValueTask.FromResult(default(object?)); + + var model = JsonSerializer.Deserialize(value, returnType); + return ValueTask.FromResult(model); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Expressions/LiteralExpression.cs b/src/core/Elsa.Core/Expressions/LiteralExpression.cs new file mode 100644 index 000000000..ac32f81b1 --- /dev/null +++ b/src/core/Elsa.Core/Expressions/LiteralExpression.cs @@ -0,0 +1,28 @@ +using Elsa.Contracts; +using Elsa.Helpers; +using Elsa.Models; + +namespace Elsa.Expressions; + +public class LiteralExpression : IExpression +{ + public LiteralExpression(object? value) => Value = value; + public object? Value { get; } +} + +public class LiteralExpression : LiteralExpression +{ + public LiteralExpression(T? value) : base(value) + { + } +} + +public class LiteralExpressionHandler : IExpressionHandler +{ + public ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) + { + var literalExpression = (LiteralExpression)expression; + var value = literalExpression.Value.ConvertTo(returnType); + return ValueTask.FromResult(value); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Expressions/VariableExpression.cs b/src/core/Elsa.Core/Expressions/VariableExpression.cs new file mode 100644 index 000000000..4a521176e --- /dev/null +++ b/src/core/Elsa.Core/Expressions/VariableExpression.cs @@ -0,0 +1,36 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Expressions; + +public class VariableExpression : IExpression +{ + public VariableExpression(Variable variable) + { + Variable = variable; + } + + public VariableExpression() + { + } + + public Variable Variable { get; set; } = default!; +} + +public class VariableExpression : VariableExpression +{ + public VariableExpression(Variable variable) : base(variable) + { + } +} + +public class VariableExpressionHandler : IExpressionHandler +{ + public ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) + { + var variableExpression = (VariableExpression)expression; + var variable = variableExpression.Variable; + var value = variable.Get(context); + return ValueTask.FromResult(value); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ActivityDriverExtensions.cs b/src/core/Elsa.Core/Extensions/ActivityDriverExtensions.cs new file mode 100644 index 000000000..da514ed3d --- /dev/null +++ b/src/core/Elsa.Core/Extensions/ActivityDriverExtensions.cs @@ -0,0 +1,19 @@ +// using System; +// using System.Reflection; +// using Elsa.Contracts; +// +// namespace Elsa.Extensions +// { +// public static class ActivityDriverExtensions +// { +// public static TDelegate GetDelegate(this IActivityDriver driver, string methodName) where TDelegate : Delegate +// { +// var driverType = driver!.GetType(); +// var resumeMethodInfo = driverType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; +// return (TDelegate)Delegate.CreateDelegate(typeof(TDelegate), driver, resumeMethodInfo); +// } +// +// public static ExecuteActivityDelegate GetResumeActivityDelegate(this IActivityDriver driver, string resumeMethodName) => driver.GetDelegate(resumeMethodName); +// public static ActivityCompletionCallback GetActivityCompletionCallback(this IActivityDriver driver, string completionMethodName) => driver.GetDelegate(completionMethodName); +// } +// } \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ActivityExtensions.cs b/src/core/Elsa.Core/Extensions/ActivityExtensions.cs new file mode 100644 index 000000000..3b8ab7fb0 --- /dev/null +++ b/src/core/Elsa.Core/Extensions/ActivityExtensions.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Extensions; + +public static class ActivityExtensions +{ + public static IEnumerable GetInputs(this IActivity activity) + { + var inputProps = activity.GetType().GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType)).ToList(); + + var query = + from inputProp in inputProps + select (Input?)inputProp.GetValue(activity) + into input + where input != null + select input; + + return query.Select(x => x!).ToList(); + } + + public static IEnumerable GetOutputs(this IActivity activity) + { + var outputProps = activity.GetType().GetProperties().Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).ToList(); + + var query = + from outputProp in outputProps + select (Output?)outputProp.GetValue(activity) + into output + where output != null + select output; + + return query.Select(x => x!).ToList(); + } + + + + public static IEnumerable GetVariables(this IActivity activity) + { + var properties = activity.GetType().GetProperties(); + var variableProps = properties.Where(x => typeof(Variable).IsAssignableFrom(x.PropertyType)).ToList(); + var variablesProps = properties.Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType)).ToList(); + var variables = variableProps.Select(x => (Variable?)x.GetValue(activity)).Where(x => x != null).Select(x => x!).ToList(); + var manyVariables = variablesProps.Select(x => (IEnumerable?)x.GetValue(activity)).Where(x => x != null).SelectMany(x => x!).ToList(); + + return variables.Concat(manyVariables).ToList(); + } + + public static TDelegate GetDelegate(this IActivity driver, string methodName) where TDelegate : Delegate + { + var driverType = driver!.GetType(); + var resumeMethodInfo = driverType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + return (TDelegate)Delegate.CreateDelegate(typeof(TDelegate), driver, resumeMethodInfo); + } + + public static ExecuteActivityDelegate GetResumeActivityDelegate(this IActivity driver, string resumeMethodName) => driver.GetDelegate(resumeMethodName); + public static ActivityCompletionCallback GetActivityCompletionCallback(this IActivity driver, string completionMethodName) => driver.GetDelegate(completionMethodName); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/NodeExtensions.cs b/src/core/Elsa.Core/Extensions/NodeExtensions.cs new file mode 100644 index 000000000..21b8d1c0c --- /dev/null +++ b/src/core/Elsa.Core/Extensions/NodeExtensions.cs @@ -0,0 +1,34 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Extensions; + +public static class NodeExtensions +{ + public static IEnumerable GetInputs(this INode node) + { + var inputProps = node.GetType().GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType)).ToList(); + + var query = + from inputProp in inputProps + select (Input?)inputProp.GetValue(node) + into input + where input != null + select input; + + return query.Select(x => x!).ToList(); + } + + public static IEnumerable Flatten(this ActivityNode root) + { + yield return root; + + foreach (var node in root.Children) + { + var children = node.Flatten(); + + foreach (var child in children) + yield return child; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..b11d2093a --- /dev/null +++ b/src/core/Elsa.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Elsa.Contracts; +using Elsa.Options; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddExpressionHandler(this IServiceCollection services) where THandler : class, IExpressionHandler => + services.AddExpressionHandler(typeof(TExpression)); + + public static IServiceCollection AddExpressionHandler(this IServiceCollection services, Type expression) where THandler : class, IExpressionHandler + { + // Register handler with DI. + services.AddSingleton(); + + // Register handler with options. + services.Configure(elsa => elsa.RegisterExpressionHandler(expression)); + + return services; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Helpers/ObjectConverter.cs b/src/core/Elsa.Core/Helpers/ObjectConverter.cs new file mode 100644 index 000000000..039cca57b --- /dev/null +++ b/src/core/Elsa.Core/Helpers/ObjectConverter.cs @@ -0,0 +1,58 @@ +using System.ComponentModel; +using Elsa.Exceptions; + +namespace Elsa.Helpers; + +public static class ObjectConverter +{ + public static T? ConvertTo(this object? value) => value != null ? (T?)value.ConvertTo(typeof(T)) : default; + + public static object? ConvertTo(this object? value, Type targetType) + { + if (value == null) + return default!; + + var sourceType = value.GetType(); + + if (sourceType == targetType) + return value; + + var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + if (targetType == typeof(object)) + return value; + + if (underlyingTargetType.IsInstanceOfType(value)) + return value; + + var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType; + + if (underlyingSourceType == underlyingTargetType) + return value; + + var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType); + + if (targetTypeConverter.CanConvertFrom(underlyingSourceType)) + return targetTypeConverter.ConvertFrom(value); + + var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType); + + if (sourceTypeConverter.CanConvertTo(underlyingTargetType)) + return sourceTypeConverter.ConvertTo(value, underlyingTargetType); + + if (underlyingTargetType.IsEnum) + { + if (underlyingSourceType != typeof(string)) + return Enum.ToObject(underlyingTargetType, value); + } + + try + { + return Convert.ChangeType(value, underlyingTargetType); + } + catch (InvalidCastException e) + { + throw new TypeConversionException($"Failed to convert an object of type {sourceType} to {underlyingTargetType}", value, underlyingTargetType, e); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Helpers/TypeNameHelper.cs b/src/core/Elsa.Core/Helpers/TypeNameHelper.cs new file mode 100644 index 000000000..8eaff76ff --- /dev/null +++ b/src/core/Elsa.Core/Helpers/TypeNameHelper.cs @@ -0,0 +1,44 @@ +namespace Elsa.Helpers; + +public static class TypeNameHelper +{ + private static readonly string[] WellknownNamespaces = { "Elsa.Activities", "Elsa.Triggers" }; + + public static string? GenerateNamespace(Type activityType) => GenerateTypeNamespace(activityType); + + public static string GenerateTypeName(Type type, string? ns) + { + var typeName = type.Name; + return ns != null ? $"{ns}.{typeName}" : typeName; + } + + public static string GenerateTypeName() => GenerateTypeName(typeof(T)); + + public static string GenerateTypeName(Type type) + { + var ns = GenerateNamespace(type); + return GenerateTypeName(type, ns); + } + + public static string? GetCategoryFromNamespace(string? ns) + { + if (string.IsNullOrWhiteSpace(ns)) + return null; + + var index = ns.LastIndexOf('.'); + + return index < 0 ? ns : ns[(index + 1)..]; + } + + private static string? GenerateTypeNamespace(Type type) + { + if (type.Namespace == null) + return null; + + foreach (var wellknownNamespace in WellknownNamespaces) + if (type.Namespace.StartsWith(wellknownNamespace)) + return type.Namespace[(wellknownNamespace.Length + 1)..]; + + return type.Namespace; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/IsExternalInit.cs b/src/core/Elsa.Core/IsExternalInit.cs new file mode 100644 index 000000000..dfaa576cd --- /dev/null +++ b/src/core/Elsa.Core/IsExternalInit.cs @@ -0,0 +1,17 @@ +// // Licensed to the .NET Foundation under one or more agreements. +// // The .NET Foundation licenses this file to you under the MIT license. +// +// using System.ComponentModel; +// +// // ReSharper disable once CheckNamespace +// namespace System.Runtime.CompilerServices +// { +// /// +// /// Reserved to be used by the compiler for tracking metadata. +// /// This class should not be used by developers in source code. +// /// +// [EditorBrowsable(EditorBrowsableState.Never)] +// internal static class IsExternalInit +// { +// } +// } \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Activity.cs b/src/core/Elsa.Core/Models/Activity.cs new file mode 100644 index 000000000..b43bb2e28 --- /dev/null +++ b/src/core/Elsa.Core/Models/Activity.cs @@ -0,0 +1,33 @@ +using Elsa.Contracts; +using Elsa.Helpers; + +namespace Elsa.Models; + +public abstract class Activity : IActivity +{ + protected Activity() => NodeType = TypeNameHelper.GenerateTypeName(GetType()); + protected Activity(string activityType) => NodeType = activityType; + + public string Id { get; set; } = default!; + public string NodeType { get; set; } + public IDictionary Metadata { get; set; } = new Dictionary(); + + public virtual ValueTask ExecuteAsync(ActivityExecutionContext context) + { + Execute(context); + return ValueTask.CompletedTask; + } + + protected virtual void Execute(ActivityExecutionContext context) + { + } +} + +public abstract class ActivityWithResult : Activity +{ + public Output? Result { get; set; } +} + +public abstract class Activity : ActivityWithResult +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/ActivityExecutionContext.cs b/src/core/Elsa.Core/Models/ActivityExecutionContext.cs new file mode 100644 index 000000000..aa5f4c20e --- /dev/null +++ b/src/core/Elsa.Core/Models/ActivityExecutionContext.cs @@ -0,0 +1,107 @@ +using System.Collections.ObjectModel; +using Elsa.Contracts; + +namespace Elsa.Models; + +public class ActivityExecutionContext +{ + private readonly List _bookmarks = new(); + + public ActivityExecutionContext( + WorkflowExecutionContext workflowExecutionContext, + ActivityExecutionContext? parentActivityExecutionContext, + ExpressionExecutionContext expressionExecutionContext, + IActivity activity, + CancellationToken cancellationToken) + { + WorkflowExecutionContext = workflowExecutionContext; + ParentActivityExecutionContext = parentActivityExecutionContext; + ExpressionExecutionContext = expressionExecutionContext; + Activity = activity; + CancellationToken = cancellationToken; + Id = Guid.NewGuid().ToString(); + } + + public string Id { get; set; } + public WorkflowExecutionContext WorkflowExecutionContext { get; } + public ActivityExecutionContext? ParentActivityExecutionContext { get; internal set; } + public ExpressionExecutionContext ExpressionExecutionContext { get; } + public IActivity Activity { get; set; } + public CancellationToken CancellationToken { get; } + public IDictionary Properties { get; set; } = new Dictionary(); + public ActivityNode ActivityNode => WorkflowExecutionContext.FindNodeByActivity(Activity); + public IReadOnlyCollection Bookmarks => new ReadOnlyCollection(_bookmarks); + public bool Continue { get; set; } = true; + + public void ScheduleActivity(IActivity activity, ActivityCompletionCallback? completionCallback = default, IEnumerable? locationReferences = default, object? tag = default) => + WorkflowExecutionContext.Schedule(activity, this, completionCallback, locationReferences, tag); + + public void ScheduleActivity(IActivity activity, ActivityExecutionContext owner, ActivityCompletionCallback? completionCallback = default, IEnumerable? locationReferences = default, object? tag = default) => + WorkflowExecutionContext.Schedule(activity, owner, completionCallback, locationReferences, tag); + + public void ScheduleActivities(params IActivity[] activities) => ScheduleActivities((IEnumerable)activities); + + public void ScheduleActivities(IEnumerable activities, ActivityCompletionCallback? completionCallback = default) + { + foreach (var activity in activities) + ScheduleActivity(activity, completionCallback); + } + + public void SetBookmarks(IEnumerable bookmarks) => _bookmarks.AddRange(bookmarks); + public void SetBookmark(Bookmark bookmark) => _bookmarks.Add(bookmark); + + public void SetBookmark(string? hash, IDictionary? data = default, ExecuteActivityDelegate? callback = default) => + SetBookmark(new Bookmark( + Guid.NewGuid().ToString(), + Activity.NodeType, + hash, + Activity.Id, + Id, + data ?? new Dictionary(), + callback?.Method.Name)); + + public T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default; + public void SetProperty(string key, T value) => Properties[key] = value; + + public T UpdateProperty(string key, Func updater) + { + var value = GetProperty(key); + value = updater(value); + Properties[key] = value; + return value; + } + + public T GetRequiredService() where T : notnull => WorkflowExecutionContext.GetRequiredService(); + public T? Get(Input input) => Get(input.LocationReference); + + public object? Get(RegisterLocationReference locationReference) + { + var location = GetLocation(locationReference) ?? throw new InvalidOperationException($"No location found with ID {locationReference.Id}. Did you forget to declare a variable with a container?"); + return location.Value; + } + + public T? Get(RegisterLocationReference locationReference) + { + var value = Get(locationReference); + return value != default ? (T?)(value) : default; + } + + public void Set(RegisterLocationReference locationReference, object? value) => ExpressionExecutionContext.Set(locationReference, value); + public void Set(Output? output, object? value) => ExpressionExecutionContext.Set(output, value); + + public async Task EvaluateAsync(Input input) + { + var evaluator = GetRequiredService(); + var locationReference = input.LocationReference; + var value = await evaluator.EvaluateAsync(input, ExpressionExecutionContext); + locationReference.Set(this, value); + return (T?)value; + } + + private RegisterLocation? GetLocation(RegisterLocationReference locationReference) => + ExpressionExecutionContext.Register.TryGetLocation(locationReference.Id, out var location) + ? location + : ParentActivityExecutionContext?.GetLocation(locationReference); + + public void PreventContinuation() => Continue = false; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/ActivityNode.cs b/src/core/Elsa.Core/Models/ActivityNode.cs new file mode 100644 index 000000000..8eae543cb --- /dev/null +++ b/src/core/Elsa.Core/Models/ActivityNode.cs @@ -0,0 +1,45 @@ +using Elsa.Contracts; + +namespace Elsa.Models; + +public class ActivityNode +{ + public ActivityNode(IActivity activity) + { + Activity = activity; + } + + public string NodeId => Activity.Id; + public IActivity Activity { get; } + public ICollection Parents { get; set; } = new List(); + public ICollection Children { get; set; } = new List(); + + public IEnumerable Descendants() + { + foreach (var child in Children) + { + yield return child; + + var descendants = child.Descendants(); + + foreach (var descendant in descendants) + yield return descendant; + } + } + + public IEnumerable Ancestors() + { + foreach (var parent in Parents) + { + yield return parent; + + var ancestors = parent.Ancestors(); + + foreach (var ancestor in ancestors) + yield return ancestor; + } + } + + public IEnumerable Siblings() => Parents.SelectMany(parent => parent.Children); + public IEnumerable SiblingsAndCousins() => Parents.SelectMany(parent => parent.Descendants()); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/ActivityWorkItem.cs b/src/core/Elsa.Core/Models/ActivityWorkItem.cs new file mode 100644 index 000000000..4acdca058 --- /dev/null +++ b/src/core/Elsa.Core/Models/ActivityWorkItem.cs @@ -0,0 +1,15 @@ +namespace Elsa.Models; + +public class ActivityWorkItem +{ + public ActivityWorkItem(string activityId, Func execute, object? tag = default) + { + ActivityId = activityId; + Execute = execute; + Tag = tag; + } + + public string ActivityId { get; } + public Func Execute { get; } + public object? Tag { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Argument.cs b/src/core/Elsa.Core/Models/Argument.cs new file mode 100644 index 000000000..ea5d4f96e --- /dev/null +++ b/src/core/Elsa.Core/Models/Argument.cs @@ -0,0 +1,13 @@ +namespace Elsa.Models; + +public abstract class Argument +{ + protected Argument(RegisterLocationReference locationReference, Func? valueConverter = default) + { + LocationReference = locationReference; + ValueConverter = valueConverter; + } + + public RegisterLocationReference LocationReference { get; set; } + public Func? ValueConverter { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Bookmark.cs b/src/core/Elsa.Core/Models/Bookmark.cs new file mode 100644 index 000000000..41b16909e --- /dev/null +++ b/src/core/Elsa.Core/Models/Bookmark.cs @@ -0,0 +1,11 @@ +namespace Elsa.Models; + +public record Bookmark( + string Id, + string Name, + string? Hash, + string ActivityId, + string ActivityInstanceId, + IDictionary? Data = default, + string? CallbackMethodName = default +); \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Composite.cs b/src/core/Elsa.Core/Models/Composite.cs new file mode 100644 index 000000000..1b9144bbf --- /dev/null +++ b/src/core/Elsa.Core/Models/Composite.cs @@ -0,0 +1,17 @@ +using Elsa.Activities.Workflows; +using Elsa.Attributes; +using Elsa.Contracts; + +namespace Elsa.Models; + +public abstract class Composite : Activity +{ + [Outbound] public IActivity Root { get; protected set; } = new Sequence(); + + protected override void Execute(ActivityExecutionContext context) + { + context.ScheduleActivity(Root, OnCompletedAsync); + } + + protected virtual ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => ValueTask.CompletedTask; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/DelegateReference.cs b/src/core/Elsa.Core/Models/DelegateReference.cs new file mode 100644 index 000000000..65c333ced --- /dev/null +++ b/src/core/Elsa.Core/Models/DelegateReference.cs @@ -0,0 +1,34 @@ +namespace Elsa.Models; + +public class DelegateReference : RegisterLocationReference +{ + public DelegateReference() + { + } + + public DelegateReference(Func @delegate) => Delegate = _ => ValueTask.FromResult(@delegate()); + public DelegateReference(Func @delegate) => Delegate = x => ValueTask.FromResult(@delegate(x)); + public DelegateReference(Func> @delegate) => Delegate = @delegate; + + public Func>? Delegate { get; set; } + public override RegisterLocation Declare() => new(); +} + +public class DelegateReference : DelegateReference +{ + public DelegateReference() + { + } + + public DelegateReference(Func @delegate) : base(x => @delegate()) + { + } + + public DelegateReference(Func @delegate) : base(x => @delegate(x)) + { + } + + public DelegateReference(Func> @delegate) : base(x => @delegate(x)) + { + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/DispatchWorkflowResult.cs b/src/core/Elsa.Core/Models/DispatchWorkflowResult.cs new file mode 100644 index 000000000..050a1e470 --- /dev/null +++ b/src/core/Elsa.Core/Models/DispatchWorkflowResult.cs @@ -0,0 +1,3 @@ +namespace Elsa.Models; + +public record DispatchWorkflowResult(); \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/DynamicActivity.cs b/src/core/Elsa.Core/Models/DynamicActivity.cs new file mode 100644 index 000000000..d6c4a8852 --- /dev/null +++ b/src/core/Elsa.Core/Models/DynamicActivity.cs @@ -0,0 +1,14 @@ +using Elsa.Contracts; + +namespace Elsa.Models; + +/// +/// A dynamically provided activity with custom properties. +/// +public class DynamicActivity : Activity +{ + public IDictionary Properties { get; set; } = new Dictionary(); + public ExecuteActivityDelegate ExecuteHandler { get; set; } = _ => ValueTask.CompletedTask; + + public override ValueTask ExecuteAsync(ActivityExecutionContext context) => ExecuteHandler(context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/ElsaExpressionReference.cs b/src/core/Elsa.Core/Models/ElsaExpressionReference.cs new file mode 100644 index 000000000..fb1dc8b3d --- /dev/null +++ b/src/core/Elsa.Core/Models/ElsaExpressionReference.cs @@ -0,0 +1,15 @@ +using Elsa.Expressions; + +namespace Elsa.Models; + +public class ElsaExpressionReference : RegisterLocationReference +{ + public ElsaExpressionReference(ElsaExpression expression) + { + Expression = expression; + } + + public ElsaExpression Expression { get; } + + public override RegisterLocation Declare() => new(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/ExecuteWorkflowResult.cs b/src/core/Elsa.Core/Models/ExecuteWorkflowResult.cs new file mode 100644 index 000000000..08352a0cb --- /dev/null +++ b/src/core/Elsa.Core/Models/ExecuteWorkflowResult.cs @@ -0,0 +1,5 @@ +using Elsa.State; + +namespace Elsa.Models; + +public record ExecuteWorkflowResult(WorkflowState WorkflowState, IReadOnlyCollection Bookmarks); \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/ExpressionExecutionContext.cs b/src/core/Elsa.Core/Models/ExpressionExecutionContext.cs new file mode 100644 index 000000000..9dea97b22 --- /dev/null +++ b/src/core/Elsa.Core/Models/ExpressionExecutionContext.cs @@ -0,0 +1,35 @@ +namespace Elsa.Models; + +public class ExpressionExecutionContext +{ + public ExpressionExecutionContext(Register register, ExpressionExecutionContext? parentContext) + { + Register = register; + ParentContext = parentContext; + } + + public Register Register { get; } + public ExpressionExecutionContext? ParentContext { get; set; } + + public RegisterLocation GetLocation(RegisterLocationReference locationReference) => GetLocationInternal(locationReference) ?? throw new InvalidOperationException(); + public object Get(RegisterLocationReference locationReference) => GetLocation(locationReference).Value!; + public T Get(RegisterLocationReference locationReference) => (T)Get(locationReference); + public T? Get(Input input) => (T?)GetLocation(input.LocationReference).Value; + + public void Set(RegisterLocationReference locationReference, object? value) + { + var location = GetLocationInternal(locationReference) ?? Register.Declare(locationReference); + location.Value = value; + } + + public void Set(Output? output, object? value) + { + if (output?.LocationReference == null) + return; + + var convertedValue = output.ValueConverter?.Invoke(value) ?? value; + Set(output.LocationReference, convertedValue); + } + + private RegisterLocation? GetLocationInternal(RegisterLocationReference locationReference) => Register.TryGetLocation(locationReference.Id, out var location) ? location : ParentContext?.GetLocationInternal(locationReference); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Input.cs b/src/core/Elsa.Core/Models/Input.cs new file mode 100644 index 000000000..e463a1ff7 --- /dev/null +++ b/src/core/Elsa.Core/Models/Input.cs @@ -0,0 +1,59 @@ +using Elsa.Contracts; +using Elsa.Expressions; + +namespace Elsa.Models; + +public abstract class Input : Argument +{ + protected Input(IExpression expression, RegisterLocationReference locationReference, Type type) : base(locationReference) + { + Expression = expression; + Type = type; + } + + public IExpression Expression { get; } + public Type Type { get; set; } +} + +public class Input : Input +{ + public Input(T literal) : this(new Literal(literal)) + { + } + + public Input(Func @delegate) : this(new DelegateReference(() => @delegate())) + { + } + + public Input(Func @delegate) : this(new DelegateReference(@delegate)) + { + } + + public Input(Variable variable) : base(new VariableExpression(variable), variable, typeof(T)) + { + } + + public Input(Literal literal) : base(new LiteralExpression(literal.Value), literal, typeof(T)) + { + } + + public Input(Literal literal) : base(new LiteralExpression(literal.Value), literal, typeof(T)) + { + } + + public Input(DelegateReference delegateReference) : base(new DelegateExpression(delegateReference), delegateReference, typeof(T)) + { + } + + public Input(ElsaExpression expression) : this(new ElsaExpressionReference(expression)) + { + } + + public Input(IExpression expression, RegisterLocationReference locationReference) : base(expression, locationReference, typeof(T)) + { + } + + private Input(ElsaExpressionReference expressionReference) : base(expressionReference.Expression, expressionReference, typeof(T)) + { + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/JsonObject.cs b/src/core/Elsa.Core/Models/JsonObject.cs new file mode 100644 index 000000000..8c7768164 --- /dev/null +++ b/src/core/Elsa.Core/Models/JsonObject.cs @@ -0,0 +1,17 @@ +namespace Elsa.Models; + +public class JsonObject : RegisterLocationReference +{ + public JsonObject() + { + } + + public JsonObject(string? defaultValue) + { + DefaultValue = defaultValue; + } + + public string? Name { get; set; } + public string? DefaultValue { get; } + public override RegisterLocation Declare() => new(DefaultValue); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Literal.cs b/src/core/Elsa.Core/Models/Literal.cs new file mode 100644 index 000000000..5fb4dc7e9 --- /dev/null +++ b/src/core/Elsa.Core/Models/Literal.cs @@ -0,0 +1,31 @@ +namespace Elsa.Models; + +public class Literal : RegisterLocationReference +{ + public Literal() + { + } + + public Literal(object? value) + { + Value = value; + } + + public object? Value { get; } + public override RegisterLocation Declare() => new(); + + public static Literal From(T value) => new Literal(value); +} + +public class Literal : Literal +{ + public Literal() + { + } + + public Literal(T value) : base(value!) + { + } + + public new T? Get(ActivityExecutionContext context) => (T?)base.Get(context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/NodeIdentity.cs b/src/core/Elsa.Core/Models/NodeIdentity.cs new file mode 100644 index 000000000..cce4c2ac7 --- /dev/null +++ b/src/core/Elsa.Core/Models/NodeIdentity.cs @@ -0,0 +1,3 @@ +namespace Elsa.Models; + +//public record NodeIdentity(ActivityNode ActivityNode, string NodeName); \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Output.cs b/src/core/Elsa.Core/Models/Output.cs new file mode 100644 index 000000000..4127b8425 --- /dev/null +++ b/src/core/Elsa.Core/Models/Output.cs @@ -0,0 +1,15 @@ +namespace Elsa.Models; + +public class Output : Argument +{ + public Output(RegisterLocationReference locationReference, Func? valueConverter = default) : base(locationReference, valueConverter) + { + } +} + +public class Output : Output +{ + public Output(RegisterLocationReference locationReference, Func? valueConverter = default) : base(locationReference, valueConverter) + { + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Register.cs b/src/core/Elsa.Core/Models/Register.cs new file mode 100644 index 000000000..6703d175c --- /dev/null +++ b/src/core/Elsa.Core/Models/Register.cs @@ -0,0 +1,33 @@ +namespace Elsa.Models; + +/// +/// Represents a register of memory. +/// +public class Register +{ + public Register(IDictionary? locations = default) + { + Locations = locations ?? new Dictionary(); + } + + public IDictionary Locations { get; } + + public bool TryGetLocation(string id, out RegisterLocation location) + { + location = null!; + return Locations.TryGetValue(id, out location!); + } + + public void Declare(IEnumerable references) + { + foreach (var reference in references) + Declare(reference); + } + + public RegisterLocation Declare(RegisterLocationReference reference) + { + var location = reference.Declare(); + Locations[reference.Id] = location; + return location; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/RegisterLocation.cs b/src/core/Elsa.Core/Models/RegisterLocation.cs new file mode 100644 index 000000000..fa7e6aa57 --- /dev/null +++ b/src/core/Elsa.Core/Models/RegisterLocation.cs @@ -0,0 +1,18 @@ +namespace Elsa.Models; + +/// +/// Represents a location within a register +/// +public class RegisterLocation +{ + public RegisterLocation() + { + } + + public RegisterLocation(object? value) + { + Value = value; + } + + public object? Value { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/RegisterLocationReference.cs b/src/core/Elsa.Core/Models/RegisterLocationReference.cs new file mode 100644 index 000000000..fa8d54672 --- /dev/null +++ b/src/core/Elsa.Core/Models/RegisterLocationReference.cs @@ -0,0 +1,21 @@ +namespace Elsa.Models; + +public abstract class RegisterLocationReference +{ + protected RegisterLocationReference() + { + } + + protected RegisterLocationReference(string id) => Id = id; + + public string Id { get; set; } = default!; + public abstract RegisterLocation Declare(); + public object? Get(ExpressionExecutionContext context) => context.Get(this); + public object? Get(ActivityExecutionContext context) => Get(context.ExpressionExecutionContext); + public T? Get(ExpressionExecutionContext context) => (T?)Get(context); + public T? Get(ActivityExecutionContext context) => (T?)Get(context.ExpressionExecutionContext); + public void Set(ExpressionExecutionContext context, object? value) => context.Set(this, value); + public void Set(ActivityExecutionContext context, object? value) => Set(context.ExpressionExecutionContext, value); + + public RegisterLocation GetLocation(Register register) => register.TryGetLocation(Id, out var location) ? location : register.Declare(this); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Trigger.cs b/src/core/Elsa.Core/Models/Trigger.cs new file mode 100644 index 000000000..a45be0a98 --- /dev/null +++ b/src/core/Elsa.Core/Models/Trigger.cs @@ -0,0 +1,28 @@ +using Elsa.Contracts; + +namespace Elsa.Models; + +public class Trigger : Activity, ITrigger +{ + protected Trigger() + { + } + + protected Trigger(string triggerType) : base(triggerType) + { + } + + public string TriggerType + { + get => NodeType; + set => NodeType = value; + } + + public virtual ValueTask> GetHashInputsAsync(TriggerIndexingContext context, CancellationToken cancellationToken = default) + { + var hashes = GetHashInputs(context); + return ValueTask.FromResult(hashes); + } + + protected virtual IEnumerable GetHashInputs(TriggerIndexingContext context) => Enumerable.Empty(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/TriggerIndexingContext.cs b/src/core/Elsa.Core/Models/TriggerIndexingContext.cs new file mode 100644 index 000000000..d42a27cef --- /dev/null +++ b/src/core/Elsa.Core/Models/TriggerIndexingContext.cs @@ -0,0 +1,17 @@ +using Elsa.Contracts; + +namespace Elsa.Models; + +public class TriggerIndexingContext +{ + public WorkflowIndexingContext WorkflowIndexingContext { get; } + public ExpressionExecutionContext ExpressionExecutionContext { get; } + public ITrigger Trigger { get; } + + public TriggerIndexingContext(WorkflowIndexingContext workflowIndexingContext, ExpressionExecutionContext expressionExecutionContext, ITrigger trigger) + { + WorkflowIndexingContext = workflowIndexingContext; + ExpressionExecutionContext = expressionExecutionContext; + Trigger = trigger; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/TriggerType.cs b/src/core/Elsa.Core/Models/TriggerType.cs new file mode 100644 index 000000000..d44734dd1 --- /dev/null +++ b/src/core/Elsa.Core/Models/TriggerType.cs @@ -0,0 +1,3 @@ +namespace Elsa.Models; + +public record TriggerType(string TypeName, Type Type); \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/TypeDescriptor.cs b/src/core/Elsa.Core/Models/TypeDescriptor.cs new file mode 100644 index 000000000..05a698bc4 --- /dev/null +++ b/src/core/Elsa.Core/Models/TypeDescriptor.cs @@ -0,0 +1,3 @@ +namespace Elsa.Models; + +public record TypeDescriptor(string Name, Type Type, TypeKind Kind); \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/TypeKind.cs b/src/core/Elsa.Core/Models/TypeKind.cs new file mode 100644 index 000000000..e4e9046b8 --- /dev/null +++ b/src/core/Elsa.Core/Models/TypeKind.cs @@ -0,0 +1,11 @@ +namespace Elsa.Models; + +[Flags] +public enum TypeKind +{ + Unknown = 0, + Primitive = 1, + Object = 2, + Activity = 4, + Trigger = 8 +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Variable.cs b/src/core/Elsa.Core/Models/Variable.cs new file mode 100644 index 000000000..e427c3a7b --- /dev/null +++ b/src/core/Elsa.Core/Models/Variable.cs @@ -0,0 +1,30 @@ +namespace Elsa.Models; + +public class Variable : RegisterLocationReference +{ + public Variable() + { + } + + public Variable(object? defaultValue) + { + DefaultValue = defaultValue; + } + + public string? Name { get; set; } + public object? DefaultValue { get; } + public override RegisterLocation Declare() => new(DefaultValue); +} + +public class Variable : Variable +{ + public Variable() : base(default(T)) + { + } + + public Variable(T value) : base(value ?? default) + { + } + + public new T? Get(ExpressionExecutionContext context) => (T?)base.Get(context); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/Workflow.cs b/src/core/Elsa.Core/Models/Workflow.cs new file mode 100644 index 000000000..6c4848359 --- /dev/null +++ b/src/core/Elsa.Core/Models/Workflow.cs @@ -0,0 +1,21 @@ +using Elsa.Contracts; + +namespace Elsa.Models; + +public record Workflow( + WorkflowIdentity Identity, + WorkflowPublication Publication, + WorkflowMetadata Metadata, + IActivity Root, + ICollection Triggers) +{ + public static Workflow FromActivity(IActivity root) => new(WorkflowIdentity.VersionOne, WorkflowPublication.LatestDraft, new WorkflowMetadata(), root, new List()); + + public Workflow WithVersion(int version) => this with { Identity = Identity with { Version = version } }; + public Workflow IncrementVersion() => WithVersion(Identity.Version + 1); + public Workflow WithPublished(bool value = true) => this with { Publication = Publication with { IsPublished = value } }; + public Workflow WithLatest(bool value = true) => this with { Publication = Publication with { IsLatest = value } }; + public Workflow WithId(string value) => this with { Identity = Identity with { Id = value } }; + public Workflow WithDefinitionId(string value) => this with { Identity = Identity with { DefinitionId = value } }; + +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/WorkflowExecutionContext.cs b/src/core/Elsa.Core/Models/WorkflowExecutionContext.cs new file mode 100644 index 000000000..3f2502fb1 --- /dev/null +++ b/src/core/Elsa.Core/Models/WorkflowExecutionContext.cs @@ -0,0 +1,133 @@ +using System.Collections.ObjectModel; +using Elsa.Contracts; +using Elsa.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Models; + +//public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, IActivity Child, ActivityCompletionCallback CompletionCallback); +public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, IActivity Child, ActivityCompletionCallback CompletionCallback); + +public class WorkflowExecutionContext +{ + private static ValueTask Noop(ActivityExecutionContext context) => new(); + private readonly IServiceProvider _serviceProvider; + private readonly IList _nodes; + private readonly IList _completionCallbackEntries = new List(); + private readonly List _bookmarks = new(); + + public WorkflowExecutionContext( + IServiceProvider serviceProvider, + Workflow workflow, + ActivityNode graph, + IActivityScheduler scheduler, + Bookmark? bookmark, + ExecuteActivityDelegate? executeDelegate, + CancellationToken cancellationToken) + { + _serviceProvider = serviceProvider; + Workflow = workflow; + Graph = graph; + Id = Guid.NewGuid().ToString("N"); + _nodes = graph.Flatten().Distinct().ToList(); + Scheduler = scheduler; + Bookmark = bookmark; + ExecuteDelegate = executeDelegate; + CancellationToken = cancellationToken; + NodeIdLookup = _nodes.ToDictionary(x => x.NodeId); + NodeActivityLookup = _nodes.ToDictionary(x => x.Activity); + } + + public Workflow Workflow { get; } + public ActivityNode Graph { get; set; } + public string Id { get; set; } + public IReadOnlyCollection Nodes => new ReadOnlyCollection(_nodes); + public IDictionary NodeIdLookup { get; } + public IDictionary NodeActivityLookup { get; } + public IActivityScheduler Scheduler { get; } + public Bookmark? Bookmark { get; } + public IDictionary Properties { get; set; } = new Dictionary(); + public ExecuteActivityDelegate? ExecuteDelegate { get; set; } + public CancellationToken CancellationToken { get; } + public IReadOnlyCollection Bookmarks => new ReadOnlyCollection(_bookmarks); + public IReadOnlyCollection CompletionCallbacks => new ReadOnlyCollection(_completionCallbackEntries); + public ICollection ActivityExecutionContexts { get; set; } = new List(); + + /// + /// A volatile collection of executed activity instance IDs. This collection is reset when workflow execution starts. + /// + public ICollection ExecutionLog { get; } = new List(); + + public T GetRequiredService() where T : notnull => _serviceProvider.GetRequiredService(); + + public void Schedule(IActivity activity, ActivityExecutionContext owner, ActivityCompletionCallback? completionCallback = default, IEnumerable? locationReferences = default, object? tag = default) + { + var activityInvoker = GetRequiredService(); + var workItem = new ActivityWorkItem(activity.Id, async () => await activityInvoker.InvokeAsync(this, activity, owner, locationReferences), tag); + Scheduler.Push(workItem); + + if (completionCallback != null) + AddCompletionCallback(owner, activity, completionCallback); + } + + public void AddCompletionCallback(ActivityExecutionContext owner, IActivity child, ActivityCompletionCallback completionCallback) + { + var entry = new ActivityCompletionCallbackEntry(owner, child, completionCallback); + _completionCallbackEntries.Add(entry); + } + + public ActivityCompletionCallback? PopCompletionCallback(ActivityExecutionContext owner, IActivity child) + { + var entry = _completionCallbackEntries.FirstOrDefault(x => x.Owner == owner && x.Child == child); + + if (entry == null) + return default; + + _completionCallbackEntries.Remove(entry); + return entry.CompletionCallback; + } + + public ActivityNode FindNodeById(string nodeId) => NodeIdLookup[nodeId]; + public ActivityNode FindNodeByActivity(IActivity activity) => NodeActivityLookup[activity]; + public IActivity FindActivityById(string activityId) => FindNodeById(activityId).Activity; + public T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default(T); + public void SetProperty(string key, T value) => Properties[key] = value; + + public T UpdateProperty(string key, Func updater) + { + var value = GetProperty(key); + value = updater(value); + Properties[key] = value; + return value; + } + + public void RegisterBookmarks(IEnumerable bookmarks) => _bookmarks.AddRange(bookmarks); + + public void UnregisterBookmarks(IEnumerable bookmarks) + { + foreach (var bookmark in bookmarks) + _bookmarks.Remove(bookmark); + } + + public void ScheduleRoot() + { + var activityInvoker = GetRequiredService(); + var workItem = new ActivityWorkItem(Workflow.Root.Id, async () => await activityInvoker.InvokeAsync(this, Workflow.Root)); + Scheduler.Push(workItem); + } + + public void ScheduleBookmark(Bookmark bookmark) + { + // Construct bookmark. + var bookmarkedActivityContext = ActivityExecutionContexts.First(x => x.Id == bookmark.ActivityInstanceId); + var bookmarkedActivity = bookmarkedActivityContext.Activity; + + // Schedule the activity to resume. + var activityInvoker = GetRequiredService(); + var workItem = new ActivityWorkItem(bookmarkedActivity.Id, async () => await activityInvoker.InvokeAsync(bookmarkedActivityContext)); + Scheduler.Push(workItem); + + // If no resumption point was specified, use Noop to prevent the regular "ExecuteAsync" method to be invoked. + ExecuteDelegate = bookmark.CallbackMethodName != null ? bookmarkedActivity.GetResumeActivityDelegate(bookmark.CallbackMethodName) : Noop; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/WorkflowExecutionLogEntry.cs b/src/core/Elsa.Core/Models/WorkflowExecutionLogEntry.cs new file mode 100644 index 000000000..99dc92f52 --- /dev/null +++ b/src/core/Elsa.Core/Models/WorkflowExecutionLogEntry.cs @@ -0,0 +1,3 @@ +namespace Elsa.Models; + +public record WorkflowExecutionLogEntry( string ActivityInstanceId); \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/WorkflowIdentity.cs b/src/core/Elsa.Core/Models/WorkflowIdentity.cs new file mode 100644 index 000000000..2e5be09c5 --- /dev/null +++ b/src/core/Elsa.Core/Models/WorkflowIdentity.cs @@ -0,0 +1,6 @@ +namespace Elsa.Models; + +public record WorkflowIdentity(string DefinitionId, int Version, string Id) +{ + public static WorkflowIdentity VersionOne => new("1", 1, "1"); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/WorkflowIndexingContext.cs b/src/core/Elsa.Core/Models/WorkflowIndexingContext.cs new file mode 100644 index 000000000..785285b9b --- /dev/null +++ b/src/core/Elsa.Core/Models/WorkflowIndexingContext.cs @@ -0,0 +1,25 @@ +using Elsa.Contracts; + +namespace Elsa.Models; + +public class WorkflowIndexingContext +{ + public WorkflowIndexingContext(Workflow workflow) + { + Workflow = workflow; + } + + public Workflow Workflow { get; } + public IDictionary Registers { get; } = new Dictionary(); + + public Register GetOrCreateRegister(ITrigger activity) + { + if (!Registers.TryGetValue(activity, out var register)) + { + register = new Register(); + Registers[activity] = register; + } + + return register; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/WorkflowMetadata.cs b/src/core/Elsa.Core/Models/WorkflowMetadata.cs new file mode 100644 index 000000000..912e8df8f --- /dev/null +++ b/src/core/Elsa.Core/Models/WorkflowMetadata.cs @@ -0,0 +1,5 @@ +namespace Elsa.Models; + +public record WorkflowMetadata(string? Name = default, string? Description = default, DateTime CreatedAt = default) +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Models/WorkflowPublication.cs b/src/core/Elsa.Core/Models/WorkflowPublication.cs new file mode 100644 index 000000000..84f49aa78 --- /dev/null +++ b/src/core/Elsa.Core/Models/WorkflowPublication.cs @@ -0,0 +1,8 @@ +namespace Elsa.Models; + +public record WorkflowPublication(bool IsLatest, bool IsPublished) +{ + public static WorkflowPublication LatestAndPublished => new(true, true); + public static WorkflowPublication LatestDraft => new(true, false); + public static WorkflowPublication Draft => new(false, false); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Options/WorkflowEngineOptions.cs b/src/core/Elsa.Core/Options/WorkflowEngineOptions.cs new file mode 100644 index 000000000..1c3c4d487 --- /dev/null +++ b/src/core/Elsa.Core/Options/WorkflowEngineOptions.cs @@ -0,0 +1,29 @@ +using System.Collections.ObjectModel; + +namespace Elsa.Options; + +public class WorkflowEngineOptions +{ + private readonly IDictionary _expressionHandlers = new Dictionary(); + + public WorkflowEngineOptions() + { + ExpressionHandlers = new ReadOnlyDictionary(_expressionHandlers); + } + + public IDictionary ExpressionHandlers { get; } + + public WorkflowEngineOptions RegisterExpressionHandler(Type expression, Type handler) + { + _expressionHandlers.Add(expression, handler); + return this; + } + + public WorkflowEngineOptions RegisterExpressionHandler() => RegisterExpressionHandler(typeof(TExpression)); + + public WorkflowEngineOptions RegisterExpressionHandler(Type expression) + { + _expressionHandlers.Add(expression, typeof(THandler)); + return this; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionMiddlewareExtensions.cs b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionMiddlewareExtensions.cs new file mode 100644 index 000000000..01f6b30f4 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionMiddlewareExtensions.cs @@ -0,0 +1,20 @@ +using Elsa.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Pipelines.ActivityExecution; + +public static class ActivityExecutionMiddlewareExtensions +{ + public static IActivityExecutionBuilder UseMiddleware(this IActivityExecutionBuilder builder, params object[] args) where TMiddleware : IActivityExecutionMiddleware + { + var middleware = typeof(TMiddleware); + + return builder.Use(next => + { + var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware); + var ctorArgs = new[] { next }.Concat(args).Select(x => x!).ToArray(); + var instance = ActivatorUtilities.CreateInstance(builder.ServiceProvider, middleware, ctorArgs); + return (ActivityMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(ActivityMiddlewareDelegate), instance); + }); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionPipeline.cs b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionPipeline.cs new file mode 100644 index 000000000..efdc4242a --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionPipeline.cs @@ -0,0 +1,31 @@ +using Elsa.Contracts; +using Elsa.Models; +using Elsa.Pipelines.ActivityExecution.Components; + +namespace Elsa.Pipelines.ActivityExecution; + +public class ActivityExecutionPipeline : IActivityExecutionPipeline +{ + private readonly IServiceProvider _serviceProvider; + private ActivityMiddlewareDelegate? _pipeline; + + public ActivityExecutionPipeline(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public ActivityMiddlewareDelegate Setup(Action setup) + { + var builder = new ActivityExecutionPipelineBuilder(_serviceProvider); + setup(builder); + _pipeline = builder.Build(); + return _pipeline; + } + + public ActivityMiddlewareDelegate Pipeline => _pipeline ??= CreateDefaultPipeline(); + public async Task ExecuteAsync(ActivityExecutionContext context) => await Pipeline(context); + + private ActivityMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x + .UseActivityDrivers() + ); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionPipelineBuilder.cs b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionPipelineBuilder.cs new file mode 100644 index 000000000..598b5cafa --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityExecutionPipelineBuilder.cs @@ -0,0 +1,31 @@ +using Elsa.Contracts; + +namespace Elsa.Pipelines.ActivityExecution; + +public class ActivityExecutionPipelineBuilder : IActivityExecutionBuilder +{ + private readonly IList> _components = new List>(); + + public ActivityExecutionPipelineBuilder(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider { get; } + + public IActivityExecutionBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + public ActivityMiddlewareDelegate Build() + { + ActivityMiddlewareDelegate pipeline = _ => new ValueTask(); + + foreach (var component in _components.Reverse()) + pipeline = component(pipeline); + + return pipeline; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityMiddlewareDelegate.cs b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityMiddlewareDelegate.cs new file mode 100644 index 000000000..8e3583908 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/ActivityExecution/ActivityMiddlewareDelegate.cs @@ -0,0 +1,5 @@ +using Elsa.Models; + +namespace Elsa.Pipelines.ActivityExecution; + +public delegate ValueTask ActivityMiddlewareDelegate(ActivityExecutionContext context); \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/ActivityInvokerMiddleware.cs b/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/ActivityInvokerMiddleware.cs new file mode 100644 index 000000000..d6b96cd75 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/ActivityInvokerMiddleware.cs @@ -0,0 +1,114 @@ +using Elsa.Contracts; +using Elsa.Extensions; +using Elsa.Models; +using Microsoft.Extensions.Logging; +using Delegate = System.Delegate; + +namespace Elsa.Pipelines.ActivityExecution.Components; + +public static class InvokeDriversMiddlewareExtensions +{ + public static IActivityExecutionBuilder UseActivityDrivers(this IActivityExecutionBuilder builder) => builder.UseMiddleware(); +} + +public class ActivityInvokerMiddleware : IActivityExecutionMiddleware +{ + private readonly ActivityMiddlewareDelegate _next; + private readonly ILogger _logger; + + public ActivityInvokerMiddleware(ActivityMiddlewareDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async ValueTask InvokeAsync(ActivityExecutionContext context) + { + var workflowExecution = context.WorkflowExecutionContext; + + // Evaluate input properties. + await EvaluateInputPropertiesAsync(context); + var activity = context.Activity; + + // Execute activity. + var methodInfo = typeof(IActivity).GetMethod(nameof(IActivity.ExecuteAsync))!; + var executeDelegate = workflowExecution.ExecuteDelegate ?? (ExecuteActivityDelegate)Delegate.CreateDelegate(typeof(ExecuteActivityDelegate), activity, methodInfo); + await executeDelegate(context); + + // Record execution event. + workflowExecution.ExecutionLog.Add(new WorkflowExecutionLogEntry(context.Id)); + + // Reset execute delegate. + workflowExecution.ExecuteDelegate = null; + + // Invoke next middleware. + await _next(context); + + // Exit if any bookmarks were created. + if (context.Bookmarks.Any()) + { + // Store bookmarks. + workflowExecution.RegisterBookmarks(context.Bookmarks); + + // Block current path of execution. + return; + } + + // Complete parent chain. + await CompleteParentsAsync(context); + } + + private async Task EvaluateInputPropertiesAsync(ActivityExecutionContext context) + { + var activity = context.Activity; + var inputs = activity.GetInputs(); + var assignedInputs = inputs.Where(x => x.LocationReference != null!).ToList(); + var evaluator = context.GetRequiredService(); + var expressionExecutionContext = context.ExpressionExecutionContext; + + foreach (var input in assignedInputs) + { + var locationReference = input.LocationReference; + var value = await evaluator.EvaluateAsync(input, expressionExecutionContext); + locationReference.Set(context, value); + } + } + + private static async Task CompleteParentsAsync(ActivityExecutionContext context) + { + var workflowExecutionContext = context.WorkflowExecutionContext; + var currentContext = context; + var currentParentContext = context.ParentActivityExecutionContext; + + while (currentParentContext != null) + { + var scheduledNodes = workflowExecutionContext.Scheduler.List().Select(x => x.ActivityId).ToList(); + var descendantNodes = currentParentContext.ActivityNode.Descendants().Select(x => x.Activity.Id).Distinct().ToList(); + var hasScheduledChildren = scheduledNodes.Intersect(descendantNodes).Any(); + var @continue = currentContext.Continue; + + // Do not continue if the activity instructed not to. + if (!@continue) + return; + + if (!hasScheduledChildren) + { + // Invoke completion callbacks. + var completionCallback = workflowExecutionContext.PopCompletionCallback(currentParentContext, currentContext.Activity); + + if (completionCallback != null) + await completionCallback.Invoke(currentParentContext, currentContext); + + // Remove current activity context. + workflowExecutionContext.ActivityExecutionContexts.Remove(currentContext); + } + + // Do not continue completion callbacks of parents while there are scheduled nodes. + if (workflowExecutionContext.Scheduler.HasAny) + return; + + currentContext = currentParentContext; + currentParentContext = currentContext.ParentActivityExecutionContext; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/LoggingMiddleware.cs b/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/LoggingMiddleware.cs new file mode 100644 index 000000000..4d5b4c805 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/ActivityExecution/Components/LoggingMiddleware.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Elsa.Contracts; +using Elsa.Models; +using Microsoft.Extensions.Logging; + +namespace Elsa.Pipelines.ActivityExecution.Components; + +public class LoggingMiddleware : IActivityExecutionMiddleware +{ + private readonly ActivityMiddlewareDelegate _next; + private readonly ILogger _logger; + private readonly Stopwatch _stopwatch; + + public LoggingMiddleware(ActivityMiddlewareDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + _stopwatch = new Stopwatch(); + } + + public async ValueTask InvokeAsync(ActivityExecutionContext context) + { + var node = context.ActivityNode; + _logger.LogDebug("Executing node {Node}", node.GetType().Name); + _stopwatch.Restart(); + await _next(context); + _stopwatch.Stop(); + _logger.LogDebug("Executed node {Node} in {Elapsed}", node.GetType().Name, _stopwatch.Elapsed); + } +} + +public static class LoggingMiddlewareExtensions +{ + public static IActivityExecutionBuilder UseLogging(this IActivityExecutionBuilder builder) => builder.UseMiddleware(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/MiddlewareHelpers.cs b/src/core/Elsa.Core/Pipelines/MiddlewareHelpers.cs new file mode 100644 index 000000000..32b910c0e --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/MiddlewareHelpers.cs @@ -0,0 +1,29 @@ +using System.Reflection; + +namespace Elsa.Pipelines; + +public static class MiddlewareHelpers +{ + public static MethodInfo GetInvokeMethod(Type middleware) + { + const string invokeMethodName = "Invoke"; + const string invokeAsyncMethodName = "InvokeAsync"; + var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); + var invokeMethods = methods.Where(m => string.Equals(m.Name, invokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, invokeAsyncMethodName, StringComparison.Ordinal)).ToArray(); + + switch (invokeMethods.Length) + { + case > 1: + throw new InvalidOperationException("Multiple Invoke methods were found. Use either Invoke or InvokeAsync."); + case 0: + throw new InvalidOperationException("No Invoke methods were found. Use either Invoke or InvokeAsync"); + } + + var methodInfo = invokeMethods[0]; + + if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType) && !typeof(ValueTask).IsAssignableFrom(methodInfo.ReturnType)) + throw new InvalidOperationException($"The {methodInfo.Name} method must return Task or ValueTask"); + + return methodInfo; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/WorkflowExecution/Components/ActivitySchedulerMiddleware.cs b/src/core/Elsa.Core/Pipelines/WorkflowExecution/Components/ActivitySchedulerMiddleware.cs new file mode 100644 index 000000000..263398ae6 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/WorkflowExecution/Components/ActivitySchedulerMiddleware.cs @@ -0,0 +1,33 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Pipelines.WorkflowExecution.Components; + +public static class UseActivitySchedulerMiddlewareExtensions +{ + public static IWorkflowExecutionBuilder UseActivityScheduler(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware(); +} + +public class ActivitySchedulerMiddleware : IWorkflowExecutionMiddleware +{ + private readonly WorkflowMiddlewareDelegate _next; + public ActivitySchedulerMiddleware(WorkflowMiddlewareDelegate next) => _next = next; + + public async ValueTask InvokeAsync(WorkflowExecutionContext context) + { + var scheduler = context.Scheduler; + + // As long as there are activities scheduled, keep executing them. + while (scheduler.HasAny) + { + // Pop next work item for execution. + var currentWorkItem = scheduler.Pop(); + + // Execute work item. + await currentWorkItem.Execute(); + } + + // Invoke next middleware. + await _next(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionMiddlewareExtensions.cs b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionMiddlewareExtensions.cs new file mode 100644 index 000000000..551caf144 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionMiddlewareExtensions.cs @@ -0,0 +1,20 @@ +using Elsa.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Pipelines.WorkflowExecution; + +public static class WorkflowExecutionMiddlewareExtensions +{ + public static IWorkflowExecutionBuilder UseMiddleware(this IWorkflowExecutionBuilder builder, params object[] args) where TMiddleware: IWorkflowExecutionMiddleware + { + var middleware = typeof(TMiddleware); + + return builder.Use(next => + { + var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware); + var ctorParams = new[] { next }.Concat(args).Select(x => x!).ToArray(); + var instance = ActivatorUtilities.CreateInstance(builder.ApplicationServices, middleware, ctorParams); + return (WorkflowMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(WorkflowMiddlewareDelegate), instance); + }); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs new file mode 100644 index 000000000..55b7ced26 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs @@ -0,0 +1,28 @@ +using Elsa.Contracts; +using Elsa.Models; +using Elsa.Pipelines.WorkflowExecution.Components; + +namespace Elsa.Pipelines.WorkflowExecution; + +public class WorkflowExecutionPipeline : IWorkflowExecutionPipeline +{ + private readonly IServiceProvider _serviceProvider; + private WorkflowMiddlewareDelegate? _pipeline; + + public WorkflowExecutionPipeline(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public WorkflowMiddlewareDelegate Pipeline => _pipeline ??= CreateDefaultPipeline(); + + public WorkflowMiddlewareDelegate Setup(Action setup) + { + var builder = new WorkflowExecutionPipelineBuilder(_serviceProvider); + setup(builder); + _pipeline = builder.Build(); + return _pipeline; + } + + public async Task ExecuteAsync(WorkflowExecutionContext context) => await Pipeline(context); + + private WorkflowMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x + .UseActivityScheduler() + ); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipelineBuilder.cs b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipelineBuilder.cs new file mode 100644 index 000000000..5d6d785c4 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipelineBuilder.cs @@ -0,0 +1,41 @@ +using Elsa.Contracts; + +namespace Elsa.Pipelines.WorkflowExecution; + +public class WorkflowExecutionPipelineBuilder : IWorkflowExecutionBuilder +{ + private const string ServicesKey = "workflow-execution.Services"; + private readonly IList> _components = new List>(); + + public WorkflowExecutionPipelineBuilder(IServiceProvider serviceProvider) + { + ApplicationServices = serviceProvider; + } + + public IDictionary Properties { get; } = new Dictionary(); + + public IServiceProvider ApplicationServices + { + get => GetProperty(ServicesKey)!; + set => SetProperty(ServicesKey, value); + } + + public IWorkflowExecutionBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + public WorkflowMiddlewareDelegate Build() + { + WorkflowMiddlewareDelegate pipeline = _ => new ValueTask(); + + foreach (var component in _components.Reverse()) + pipeline = component(pipeline); + + return pipeline; + } + + private T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default(T); + private void SetProperty(string key, T value) => Properties[key] = value; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowMiddlewareDelegate.cs b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowMiddlewareDelegate.cs new file mode 100644 index 000000000..b2c7e9bf7 --- /dev/null +++ b/src/core/Elsa.Core/Pipelines/WorkflowExecution/WorkflowMiddlewareDelegate.cs @@ -0,0 +1,5 @@ +using Elsa.Models; + +namespace Elsa.Pipelines.WorkflowExecution; + +public delegate ValueTask WorkflowMiddlewareDelegate(WorkflowExecutionContext context); \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/ActivityInvoker.cs b/src/core/Elsa.Core/Services/ActivityInvoker.cs new file mode 100644 index 000000000..4ee899e37 --- /dev/null +++ b/src/core/Elsa.Core/Services/ActivityInvoker.cs @@ -0,0 +1,47 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Services; + +public class ActivityInvoker : IActivityInvoker +{ + private readonly IActivityExecutionPipeline _pipeline; + + public ActivityInvoker(IActivityExecutionPipeline pipeline) + { + _pipeline = pipeline; + } + + public async Task InvokeAsync( + WorkflowExecutionContext workflowExecutionContext, + IActivity activity, + ActivityExecutionContext? owner, + IEnumerable? locationReferences = default) + { + var cancellationToken = workflowExecutionContext.CancellationToken; + + // Get a handle to the parent execution context. + var parentActivityExecutionContext = owner; + + // Setup an activity execution context. + var register = new Register(); + var expressionExecutionContext = new ExpressionExecutionContext(register, parentActivityExecutionContext?.ExpressionExecutionContext); + var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, parentActivityExecutionContext, expressionExecutionContext, activity, cancellationToken); + + // Declare locations. + if (locationReferences != null) + register.Declare(locationReferences); + + // Push the activity context into the workflow context. + workflowExecutionContext.ActivityExecutionContexts.Add(activityExecutionContext); + + // Execute the activity execution pipeline. + await InvokeAsync(activityExecutionContext); + } + + public async Task InvokeAsync(ActivityExecutionContext activityExecutionContext) + { + // Execute the activity execution pipeline. + await _pipeline.ExecuteAsync(activityExecutionContext); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/ActivityScheduler.cs b/src/core/Elsa.Core/Services/ActivityScheduler.cs new file mode 100644 index 000000000..e36ebc53c --- /dev/null +++ b/src/core/Elsa.Core/Services/ActivityScheduler.cs @@ -0,0 +1,14 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Services; + +public class ActivityScheduler : IActivityScheduler +{ + private readonly Stack _stack = new(); + + public bool HasAny => _stack.Any(); + public void Push(ActivityWorkItem activity) => _stack.Push(activity); + public ActivityWorkItem Pop() => _stack.Pop(); + public IEnumerable List() => _stack.ToList(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/ActivitySchedulerFactory.cs b/src/core/Elsa.Core/Services/ActivitySchedulerFactory.cs new file mode 100644 index 000000000..7bffc053d --- /dev/null +++ b/src/core/Elsa.Core/Services/ActivitySchedulerFactory.cs @@ -0,0 +1,8 @@ +using Elsa.Contracts; + +namespace Elsa.Services; + +public class ActivitySchedulerFactory : IActivitySchedulerFactory +{ + public IActivityScheduler CreateScheduler() => new ActivityScheduler(); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/ActivityWalker.cs b/src/core/Elsa.Core/Services/ActivityWalker.cs new file mode 100644 index 000000000..329a13f84 --- /dev/null +++ b/src/core/Elsa.Core/Services/ActivityWalker.cs @@ -0,0 +1,54 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Services; + +public class ActivityWalker : IActivityWalker +{ + private readonly IEnumerable _portResolvers; + + public ActivityWalker(IEnumerable portResolvers) + { + _portResolvers = portResolvers.OrderByDescending(x => x.Priority).ToList(); + } + + public ActivityNode Walk(IActivity activity) + { + var collectedActivities = new HashSet(new[] { activity }); + var graph = new ActivityNode(activity); + var collectedNodes = new HashSet(new[] { graph }); + WalkRecursive((graph, activity), collectedActivities, collectedNodes); + return graph; + } + + private void WalkRecursive((ActivityNode Node, IActivity Activity) pair, HashSet collectedActivities, HashSet collectedNodes) + { + WalkPortsRecursive(pair, collectedActivities, collectedNodes); + } + + private void WalkPortsRecursive((ActivityNode Node, IActivity Activity) pair, HashSet collectedActivities, HashSet collectedNodes) + { + var resolver = _portResolvers.FirstOrDefault(x => x.GetSupportsActivity(pair.Activity)); + + if (resolver == null) + return; + + var ports = resolver.GetPorts(pair.Activity); + + foreach (var port in ports) + { + var childNode = collectedNodes.FirstOrDefault(x => x.Activity == port); + + if (childNode == null) + { + childNode = new ActivityNode(port); + collectedNodes.Add(childNode); + } + + childNode.Parents.Add(pair.Node); + pair.Node.Children.Add(childNode); + collectedActivities.Add(port); + WalkRecursive((childNode, port), collectedActivities, collectedNodes); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/ExpressionEvaluator.cs b/src/core/Elsa.Core/Services/ExpressionEvaluator.cs new file mode 100644 index 000000000..6a9de0f84 --- /dev/null +++ b/src/core/Elsa.Core/Services/ExpressionEvaluator.cs @@ -0,0 +1,27 @@ +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Services; + +public class ExpressionEvaluator : IExpressionEvaluator +{ + private readonly IExpressionHandlerRegistry _registry; + + public ExpressionEvaluator(IExpressionHandlerRegistry registry) + { + _registry = registry; + } + + public async ValueTask EvaluateAsync(IExpression input, ExpressionExecutionContext context) => (T?)await EvaluateAsync(input, typeof(T), context); + + public async ValueTask EvaluateAsync(IExpression input, Type returnType, ExpressionExecutionContext context) + { + var handler = _registry.GetHandler(input); + + if (handler != null) + return await handler.EvaluateAsync(input, returnType, context); + + var expressionType = input.GetType().GetGenericTypeDefinition(); + throw new InvalidOperationException($"Could not find handler for expression type {expressionType}"); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/ExpressionHandlerRegistry.cs b/src/core/Elsa.Core/Services/ExpressionHandlerRegistry.cs new file mode 100644 index 000000000..0daf4119b --- /dev/null +++ b/src/core/Elsa.Core/Services/ExpressionHandlerRegistry.cs @@ -0,0 +1,31 @@ +using Elsa.Contracts; +using Elsa.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Elsa.Services; + +public class ExpressionHandlerRegistry : IExpressionHandlerRegistry +{ + private readonly IServiceProvider _serviceProvider; + + public ExpressionHandlerRegistry(IOptions options, IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + Dictionary = new Dictionary(options.Value.ExpressionHandlers); + } + + private IDictionary Dictionary { get; } + + public void Register(Type expression, Type handler) => Dictionary.Add(expression, handler); + + public IExpressionHandler? GetHandler(IExpression input) + { + var expressionType = input.GetType(); + + if (!Dictionary.TryGetValue(expressionType, out var handlerType)) + return null; + + return (IExpressionHandler)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, handlerType); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/Hasher.cs b/src/core/Elsa.Core/Services/Hasher.cs new file mode 100644 index 000000000..4a726c291 --- /dev/null +++ b/src/core/Elsa.Core/Services/Hasher.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Elsa.Contracts; + +namespace Elsa.Services; + +public class Hasher : IHasher +{ + public string Hash(object value) + { + var options = new JsonSerializerOptions + { + // Enables serialization of ValueTuples, which use fields instead of properties. + IncludeFields = true + }; + + var json = JsonSerializer.Serialize(value, options); + return Hash(json); + } + + private static string Hash(string input) + { + using var sha = HashAlgorithm.Create(HashAlgorithmName.SHA256.ToString())!; + return Hash(sha, input); + } + + private static string Hash(HashAlgorithm hashAlgorithm, string input) + { + var data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexString(data); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/IdentityGenerator.cs b/src/core/Elsa.Core/Services/IdentityGenerator.cs new file mode 100644 index 000000000..33b907d2d --- /dev/null +++ b/src/core/Elsa.Core/Services/IdentityGenerator.cs @@ -0,0 +1,8 @@ +using Elsa.Contracts; + +namespace Elsa.Services; + +public class IdentityGenerator : IIdentityGenerator +{ + public string GenerateId() => Guid.NewGuid().ToString("N"); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/IdentityGraphService.cs b/src/core/Elsa.Core/Services/IdentityGraphService.cs new file mode 100644 index 000000000..a1ecd5b84 --- /dev/null +++ b/src/core/Elsa.Core/Services/IdentityGraphService.cs @@ -0,0 +1,110 @@ +using Elsa.Contracts; +using Elsa.Extensions; +using Elsa.Models; + +namespace Elsa.Services; + +public class IdentityGraphService : IIdentityGraphService +{ + private readonly IActivityWalker _activityWalker; + + public IdentityGraphService(IActivityWalker activityWalker) + { + _activityWalker = activityWalker; + } + + public void AssignIdentities(Workflow workflow) + { + AssignIdentities(workflow.Root); + + var triggers = workflow.Triggers; + + if (triggers == null) + return; + + foreach (var trigger in triggers) + { + if(trigger is IActivity activity) + AssignIdentities(activity); + } + } + + public void AssignIdentities(IActivity root) + { + var graph = _activityWalker.Walk(root); + AssignIdentities(graph); + } + + public void AssignIdentities(ActivityNode root) + { + var identityCounters = new Dictionary(); + var list = root.Flatten(); + + foreach (var node in list) + { + node.Activity.Id = CreateId(node, identityCounters); + AssignInputOutputs(node.Activity); + AssignVariables(node.Activity); + } + } + + private void AssignInputOutputs(IActivity activity) + { + var inputs = activity.GetInputs(); + var assignedInputs = inputs.Where(x => x.LocationReference != null! && x.LocationReference.Id == null!).ToList(); + var seed = 0; + + foreach (var input in assignedInputs) + { + var locationReference = input.LocationReference; + locationReference.Id = $"{activity.Id}:input-{++seed}"; + } + + seed = 0; + var outputs = activity.GetOutputs(); + var assignedOutputs = outputs.Where(x => x.LocationReference != null! && x.LocationReference.Id == null!).ToList(); + + foreach (var output in assignedOutputs) + { + var locationReference = output.LocationReference; + locationReference.Id = $"{activity.Id}:output-{++seed}"; + } + } + + private void AssignVariables(IActivity activity) + { + var variables = activity.GetVariables(); + var seed = 0; + + foreach (var variable in variables) + variable.Id = variable.Name != null! ? variable.Name : $"{activity.Id}:variable-{++seed}"; + } + + private string CreateId(ActivityNode activityNode, IDictionary identityCounters) + { + if (!string.IsNullOrWhiteSpace(activityNode.NodeId)) + return activityNode.NodeId; + + var type = activityNode.Activity.NodeType; + var index = GetNextIndexFor(type, identityCounters); + var name = $"{Camelize(type)}{index + 1}"; + return name; + } + + private int GetNextIndexFor(string nodeType, IDictionary identityCounters) + { + if (!identityCounters.TryGetValue(nodeType, out var index)) + { + identityCounters[nodeType] = index; + } + else + { + index = identityCounters[nodeType] + 1; + identityCounters[nodeType] = index; + } + + return index; + } + + private string Camelize(string symbol) => char.ToLowerInvariant(symbol[0]) + symbol[1..]; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/SystemClock.cs b/src/core/Elsa.Core/Services/SystemClock.cs new file mode 100644 index 000000000..3fc658e0b --- /dev/null +++ b/src/core/Elsa.Core/Services/SystemClock.cs @@ -0,0 +1,8 @@ +using Elsa.Contracts; + +namespace Elsa.Services; + +public class SystemClock : ISystemClock +{ + public DateTime UtcNow => DateTime.UtcNow; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowEngine.cs b/src/core/Elsa.Core/Services/WorkflowEngine.cs new file mode 100644 index 000000000..eeb9c752f --- /dev/null +++ b/src/core/Elsa.Core/Services/WorkflowEngine.cs @@ -0,0 +1,120 @@ +using Elsa.Contracts; +using Elsa.Models; +using Elsa.State; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Services; + +public class WorkflowEngine : IWorkflowEngine +{ + private static ValueTask Noop(ActivityExecutionContext context) => new(); + + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IActivityWalker _activityWalker; + private readonly IWorkflowExecutionPipeline _pipeline; + private readonly IWorkflowStateSerializer _workflowStateSerializer; + private readonly IIdentityGraphService _identityGraphService; + private readonly IActivitySchedulerFactory _schedulerFactory; + + public WorkflowEngine( + IServiceScopeFactory serviceScopeFactory, + IActivityWalker activityWalker, + IWorkflowExecutionPipeline pipeline, + IWorkflowStateSerializer workflowStateSerializer, + IIdentityGraphService identityGraphService, + IActivitySchedulerFactory schedulerFactory) + { + _serviceScopeFactory = serviceScopeFactory; + _activityWalker = activityWalker; + _pipeline = pipeline; + _workflowStateSerializer = workflowStateSerializer; + _identityGraphService = identityGraphService; + _schedulerFactory = schedulerFactory; + } + + public async Task ExecuteAsync(Workflow workflow, CancellationToken cancellationToken = default) + { + // Create a child scope. + using var scope = _serviceScopeFactory.CreateScope(); + + // Setup a workflow execution context. + var workflowExecutionContext = CreateWorkflowExecutionContext(scope.ServiceProvider, workflow, default, default, default, cancellationToken); + + // Schedule the first node. + workflowExecutionContext.ScheduleRoot(); + + return await ExecuteAsync(workflowExecutionContext); + } + + public async Task ExecuteAsync(Workflow workflow, WorkflowState workflowState, CancellationToken cancellationToken = default) + { + // Create a child scope. + using var scope = _serviceScopeFactory.CreateScope(); + + // Create workflow execution context. + var workflowExecutionContext = CreateWorkflowExecutionContext(scope.ServiceProvider, workflow, workflowState, default, default, cancellationToken); + + // Schedule the first node. + workflowExecutionContext.ScheduleRoot(); + + return await ExecuteAsync(workflowExecutionContext); + } + + public async Task ExecuteAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmark, CancellationToken cancellationToken = default) + { + // Create a child scope. + using var scope = _serviceScopeFactory.CreateScope(); + + // Create workflow execution context. + var workflowExecutionContext = CreateWorkflowExecutionContext(scope.ServiceProvider, workflow, workflowState, bookmark, default, cancellationToken); + + if (bookmark != null) + workflowExecutionContext.ScheduleBookmark(bookmark); + + return await ExecuteAsync(workflowExecutionContext); + } + + public async Task ExecuteAsync(WorkflowExecutionContext workflowExecutionContext) + { + // Execute the activity execution pipeline. + await _pipeline.ExecuteAsync(workflowExecutionContext); + + // Extract workflow state. + var workflowState = _workflowStateSerializer.ReadState(workflowExecutionContext); + + // Return workflow execution result containing state + bookmarks. + return new ExecuteWorkflowResult(workflowState, workflowExecutionContext.Bookmarks); + } + + public WorkflowExecutionContext CreateWorkflowExecutionContext( + IServiceProvider serviceProvider, + Workflow workflow, + WorkflowState? workflowState, + Bookmark? bookmark, + ExecuteActivityDelegate? executeActivityDelegate, + CancellationToken cancellationToken) + { + var root = workflow.Root; + + // Build graph. + var graph = _activityWalker.Walk(root); + + // Assign identities. + _identityGraphService.AssignIdentities(graph); + + // Create scheduler. + var scheduler = _schedulerFactory.CreateScheduler(); + + // Setup a workflow execution context. + var workflowExecutionContext = new WorkflowExecutionContext(serviceProvider, workflow, graph, scheduler, bookmark, executeActivityDelegate, cancellationToken); + + // Restore workflow execution context from state, if provided. + if (workflowState != null) + { + var workflowStateService = serviceProvider.GetRequiredService(); + workflowStateService.WriteState(workflowExecutionContext, workflowState); + } + + return workflowExecutionContext; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowStateSerializer.cs b/src/core/Elsa.Core/Services/WorkflowStateSerializer.cs new file mode 100644 index 000000000..f37878044 --- /dev/null +++ b/src/core/Elsa.Core/Services/WorkflowStateSerializer.cs @@ -0,0 +1,162 @@ +using System.Reflection; +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Extensions; +using Elsa.Models; +using Elsa.State; +using Microsoft.Extensions.Logging; + +namespace Elsa.Services; + +public class WorkflowStateSerializer : IWorkflowStateSerializer +{ + private readonly ILogger _logger; + + public WorkflowStateSerializer(ILogger logger) + { + _logger = logger; + } + + public WorkflowState ReadState(WorkflowExecutionContext workflowExecutionContext) + { + var state = new WorkflowState + { + Id = workflowExecutionContext.Id + }; + + //GetOutput(state, workflowExecutionContext); + GetProperties(state, workflowExecutionContext); + GetCompletionCallbacks(state, workflowExecutionContext); + GetActivityExecutionContexts(state, workflowExecutionContext); + + return state; + } + + public void WriteState(WorkflowExecutionContext workflowExecutionContext, WorkflowState state) + { + workflowExecutionContext.Id = state.Id; + //SetOutput(state, workflowExecutionContext); + SetProperties(state, workflowExecutionContext); + SetActivityExecutionContexts(state, workflowExecutionContext); + SetCompletionCallbacks(state, workflowExecutionContext); + } + + private void GetProperties(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + state.Properties = workflowExecutionContext.Properties; + } + + private void SetProperties(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + workflowExecutionContext.Properties = state.Properties; + } + + private void GetOutput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + foreach (var node in workflowExecutionContext.Nodes) + GetOutput(state, node); + } + + private void GetOutput(WorkflowState state, ActivityNode activityNode) + { + var output = GetOutputFrom(activityNode); + + if (output.Any()) + state.ActivityOutput.Add(activityNode.NodeId, output); + } + + private void SetOutput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + foreach (var nodeEntry in state.ActivityOutput) + { + var nodeId = nodeEntry.Key; + var node = workflowExecutionContext.FindNodeById(nodeId); + var nodeType = node.GetType(); + + foreach (var outputEntry in nodeEntry.Value) + { + var propertyName = outputEntry.Key; + var propertyValue = outputEntry.Value; + var propertyInfo = nodeType.GetProperty(propertyName, BindingFlags.Public)!; + propertyInfo.SetValue(node, propertyValue); + } + } + } + + private void SetCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + foreach (var completionCallbackEntry in state.CompletionCallbacks) + { + var owner = workflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == completionCallbackEntry.OwnerId); + var child = workflowExecutionContext.FindNodeById(completionCallbackEntry.ChildId).Activity; + var callbackName = completionCallbackEntry.MethodName; + var callbackDelegate = owner.Activity.GetActivityCompletionCallback(callbackName); + workflowExecutionContext.AddCompletionCallback(owner, child, callbackDelegate); + } + } + + private void GetCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + var completionCallbacks = workflowExecutionContext.CompletionCallbacks.Select(x => new CompletionCallbackState(x.Owner.Id, x.Child.Id, x.CompletionCallback.Method.Name)); + state.CompletionCallbacks = completionCallbacks.ToList(); + } + + private void GetActivityExecutionContexts(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + ActivityExecutionContextState CreateActivityExecutionContextState(ActivityExecutionContext activityExecutionContext) + { + var registerState = new RegisterState(activityExecutionContext.ExpressionExecutionContext.Register.Locations); + var activityExecutionContextState = new ActivityExecutionContextState + { + Id = activityExecutionContext.Id, + ScheduledActivityId = activityExecutionContext.Activity.Id, + OwnerActivityId = activityExecutionContext.ParentActivityExecutionContext?.Activity.Id, + Properties = activityExecutionContext.Properties, + Register = registerState + }; + return activityExecutionContextState; + } + + // Create a tupled list of contexts and states. + var tuples = workflowExecutionContext.ActivityExecutionContexts.Reverse().Select(x => (x, CreateActivityExecutionContextState(x))).ToList(); + + // Construct hierarchy. + foreach (var tuple in tuples) + tuple.Item2.ParentActivityExecutionContext = tuples.FirstOrDefault(x => tuple.x.ParentActivityExecutionContext == x.x).Item2; + + state.ActivityExecutionContexts = tuples.Select(x => x.Item2).ToList(); + } + + private void SetActivityExecutionContexts(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + ActivityExecutionContext CreateActivityExecutionContext(ActivityExecutionContextState activityExecutionContextState) + { + var activity = workflowExecutionContext.FindActivityById(activityExecutionContextState.ScheduledActivityId); + var register = new Register(activityExecutionContextState.Register.Locations); + var expressionExecutionContext = new ExpressionExecutionContext(register, default); + var properties = activityExecutionContextState.Properties; + var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, default, expressionExecutionContext, activity, workflowExecutionContext.CancellationToken) + { + Id = activityExecutionContextState.Id, + Properties = properties + }; + return activityExecutionContext; + } + + // Create a tupled list of states and contexts. + var tuples = state.ActivityExecutionContexts.Select(x => (x, CreateActivityExecutionContext(x))).ToList(); + + // Reconstruct hierarchy. + foreach (var tuple in tuples) + { + var activityExecutionContext = tuple.Item2; + activityExecutionContext.ParentActivityExecutionContext = tuples.FirstOrDefault(x => tuple.x.ParentActivityExecutionContext == x.x).Item2; + activityExecutionContext.ExpressionExecutionContext.ParentContext = activityExecutionContext.ParentActivityExecutionContext?.ExpressionExecutionContext; + } + + workflowExecutionContext.ActivityExecutionContexts = new List(tuples.Select(x => x.Item2)); + } + + private IDictionary GetOutputFrom(ActivityNode activityNode) => + activityNode.GetType().GetProperties(BindingFlags.Public).Where(x => x.GetCustomAttribute() != null).ToDictionary(x => x.Name, x => (object?)x.GetValue(activityNode)); +} \ No newline at end of file diff --git a/src/core/Elsa.Core/State/ActivityExecutionContextState.cs b/src/core/Elsa.Core/State/ActivityExecutionContextState.cs new file mode 100644 index 000000000..321c9fd18 --- /dev/null +++ b/src/core/Elsa.Core/State/ActivityExecutionContextState.cs @@ -0,0 +1,15 @@ +namespace Elsa.State; + +public class ActivityExecutionContextState +{ + public ActivityExecutionContextState() + { + } + + public string Id { get; set; } = default!; + public ActivityExecutionContextState? ParentActivityExecutionContext { get; set; } + public string ScheduledActivityId { get; set; } = default!; + public string? OwnerActivityId { get; set; } + public IDictionary Properties { get; set; } = new Dictionary(); + public RegisterState Register { get; set; } = default!; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/State/CompletionCallbackState.cs b/src/core/Elsa.Core/State/CompletionCallbackState.cs new file mode 100644 index 000000000..7eb33783d --- /dev/null +++ b/src/core/Elsa.Core/State/CompletionCallbackState.cs @@ -0,0 +1,3 @@ +namespace Elsa.State; + +public record CompletionCallbackState(string OwnerId, string ChildId, string MethodName); \ No newline at end of file diff --git a/src/core/Elsa.Core/State/RegisterState.cs b/src/core/Elsa.Core/State/RegisterState.cs new file mode 100644 index 000000000..eb81af13d --- /dev/null +++ b/src/core/Elsa.Core/State/RegisterState.cs @@ -0,0 +1,13 @@ +using Elsa.Models; + +namespace Elsa.State; + +public class RegisterState +{ + public RegisterState(IDictionary locations) + { + Locations = locations; + } + + public IDictionary Locations { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/State/WorkflowState.cs b/src/core/Elsa.Core/State/WorkflowState.cs new file mode 100644 index 000000000..982da5a5b --- /dev/null +++ b/src/core/Elsa.Core/State/WorkflowState.cs @@ -0,0 +1,10 @@ +namespace Elsa.State; + +public class WorkflowState +{ + public string Id { get; set; } = default!; + public IDictionary> ActivityOutput { get; set; } = new Dictionary>(); + public ICollection CompletionCallbacks { get; set; } = new List(); + public ICollection ActivityExecutionContexts { get; set; } = new List(); + public IDictionary Properties { get; set; } = new Dictionary(); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IActivityDescriber.cs b/src/core/Elsa.Management/Contracts/IActivityDescriber.cs new file mode 100644 index 000000000..d2611fea1 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IActivityDescriber.cs @@ -0,0 +1,8 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface IActivityDescriber +{ + ValueTask DescribeActivityAsync(Type activityType, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IActivityFactory.cs b/src/core/Elsa.Management/Contracts/IActivityFactory.cs new file mode 100644 index 000000000..5ffeca008 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IActivityFactory.cs @@ -0,0 +1,9 @@ +using Elsa.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface IActivityFactory +{ + IActivity Create(Type type, ActivityConstructorContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IActivityPropertyDefaultValueProvider.cs b/src/core/Elsa.Management/Contracts/IActivityPropertyDefaultValueProvider.cs new file mode 100644 index 000000000..cfcb7acbc --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IActivityPropertyDefaultValueProvider.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Elsa.Management.Contracts; + +public interface IActivityPropertyDefaultValueProvider +{ + object GetDefaultValue(PropertyInfo property); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IActivityProvider.cs b/src/core/Elsa.Management/Contracts/IActivityProvider.cs new file mode 100644 index 000000000..a64eb67b7 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IActivityProvider.cs @@ -0,0 +1,8 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface IActivityProvider +{ + ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IActivityRegistry.cs b/src/core/Elsa.Management/Contracts/IActivityRegistry.cs new file mode 100644 index 000000000..98c5343f7 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IActivityRegistry.cs @@ -0,0 +1,15 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface IActivityRegistry +{ + void Add(object provider, ActivityDescriptor descriptor); + void AddMany(object provider, IEnumerable descriptors); + void Clear(); + void ClearProvider(object provider); + IEnumerable ListAll(); + IEnumerable ListByProvider(object provider); + ActivityDescriptor? Find(Func predicate); + ActivityDescriptor? Find(string activityType); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IActivityRegistryPopulator.cs b/src/core/Elsa.Management/Contracts/IActivityRegistryPopulator.cs new file mode 100644 index 000000000..ac2283e8e --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IActivityRegistryPopulator.cs @@ -0,0 +1,6 @@ +namespace Elsa.Management.Contracts; + +public interface IActivityRegistryPopulator +{ + ValueTask PopulateRegistryAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IExpressionSyntaxProvider.cs b/src/core/Elsa.Management/Contracts/IExpressionSyntaxProvider.cs new file mode 100644 index 000000000..902ff9179 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IExpressionSyntaxProvider.cs @@ -0,0 +1,8 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface IExpressionSyntaxProvider +{ + ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IExpressionSyntaxRegistry.cs b/src/core/Elsa.Management/Contracts/IExpressionSyntaxRegistry.cs new file mode 100644 index 000000000..69552eb8f --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IExpressionSyntaxRegistry.cs @@ -0,0 +1,12 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface IExpressionSyntaxRegistry +{ + void Add(ExpressionSyntaxDescriptor descriptor); + void AddMany(IEnumerable descriptors); + IEnumerable ListAll(); + ExpressionSyntaxDescriptor? Find(Func predicate); + ExpressionSyntaxDescriptor? Find(string syntax); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IExpressionSyntaxRegistryPopulator.cs b/src/core/Elsa.Management/Contracts/IExpressionSyntaxRegistryPopulator.cs new file mode 100644 index 000000000..e9d1b25c3 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IExpressionSyntaxRegistryPopulator.cs @@ -0,0 +1,6 @@ +namespace Elsa.Management.Contracts; + +public interface IExpressionSyntaxRegistryPopulator +{ + ValueTask PopulateRegistryAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IPropertyDefaultValueResolver.cs b/src/core/Elsa.Management/Contracts/IPropertyDefaultValueResolver.cs new file mode 100644 index 000000000..2647f0d91 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IPropertyDefaultValueResolver.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Elsa.Management.Contracts; + +public interface IPropertyDefaultValueResolver +{ + object? GetDefaultValue(PropertyInfo activityPropertyInfo); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IPropertyOptionsResolver.cs b/src/core/Elsa.Management/Contracts/IPropertyOptionsResolver.cs new file mode 100644 index 000000000..a624a4743 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IPropertyOptionsResolver.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Elsa.Management.Contracts; + +public interface IPropertyOptionsResolver +{ + object? GetOptions(PropertyInfo propertyInfo); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/ITriggerDescriber.cs b/src/core/Elsa.Management/Contracts/ITriggerDescriber.cs new file mode 100644 index 000000000..ddc958756 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/ITriggerDescriber.cs @@ -0,0 +1,8 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface ITriggerDescriber +{ + ValueTask DescribeTriggerAsync(Type triggerType, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/ITriggerFactory.cs b/src/core/Elsa.Management/Contracts/ITriggerFactory.cs new file mode 100644 index 000000000..f60b62dd8 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/ITriggerFactory.cs @@ -0,0 +1,9 @@ +using Elsa.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface ITriggerFactory +{ + ITrigger Create(Type type, TriggerConstructorContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/ITriggerProvider.cs b/src/core/Elsa.Management/Contracts/ITriggerProvider.cs new file mode 100644 index 000000000..d3c404cf7 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/ITriggerProvider.cs @@ -0,0 +1,8 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface ITriggerProvider +{ + ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/ITriggerRegistry.cs b/src/core/Elsa.Management/Contracts/ITriggerRegistry.cs new file mode 100644 index 000000000..4ac3a4b0b --- /dev/null +++ b/src/core/Elsa.Management/Contracts/ITriggerRegistry.cs @@ -0,0 +1,15 @@ +using Elsa.Management.Models; + +namespace Elsa.Management.Contracts; + +public interface ITriggerRegistry +{ + void Add(object provider, TriggerDescriptor descriptor); + void AddMany(object provider, IEnumerable descriptors); + void Clear(); + void ClearProvider(object provider); + IEnumerable ListAll(); + IEnumerable ListByProvider(object provider); + TriggerDescriptor? Find(Func predicate); + TriggerDescriptor? Find(string triggerType); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/ITriggerRegistryPopulator.cs b/src/core/Elsa.Management/Contracts/ITriggerRegistryPopulator.cs new file mode 100644 index 000000000..3363a5724 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/ITriggerRegistryPopulator.cs @@ -0,0 +1,6 @@ +namespace Elsa.Management.Contracts; + +public interface ITriggerRegistryPopulator +{ + ValueTask PopulateRegistryAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IWellKnownTypeRegistry.cs b/src/core/Elsa.Management/Contracts/IWellKnownTypeRegistry.cs new file mode 100644 index 000000000..d6712d38d --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IWellKnownTypeRegistry.cs @@ -0,0 +1,13 @@ +namespace Elsa.Management.Contracts; + +public interface IWellKnownTypeRegistry +{ + void RegisterType(Type type, string alias); + bool TryGetAlias(Type type, out string alias); + bool TryGetType(string alias, out Type type); +} + +public static class WellKnowTypeRegistryExtensions +{ + public static void RegisterType(this IWellKnownTypeRegistry registry, string alias) => registry.RegisterType(typeof(T), alias); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/IWorkflowPublisher.cs b/src/core/Elsa.Management/Contracts/IWorkflowPublisher.cs new file mode 100644 index 000000000..e83c9b9a8 --- /dev/null +++ b/src/core/Elsa.Management/Contracts/IWorkflowPublisher.cs @@ -0,0 +1,17 @@ +using Elsa.Models; + +namespace Elsa.Management.Contracts +{ + public interface IWorkflowPublisher + { + Workflow New(); + Task PublishAsync(string definitionId, CancellationToken cancellationToken = default); + Task PublishAsync(Workflow workflow, CancellationToken cancellationToken = default); + Task RetractAsync(string definitionId, CancellationToken cancellationToken = default); + Task RetractAsync(Workflow workflow, CancellationToken cancellationToken = default); + Task GetDraftAsync(string definitionId, CancellationToken cancellationToken= default); + Task SaveDraftAsync(Workflow workflow, CancellationToken cancellationToken = default); + Task DeleteAsync(string definitionId, CancellationToken cancellationToken = default); + Task DeleteAsync(Workflow workflow, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Contracts/WellKnownTypeRegistry.cs b/src/core/Elsa.Management/Contracts/WellKnownTypeRegistry.cs new file mode 100644 index 000000000..84183dc0c --- /dev/null +++ b/src/core/Elsa.Management/Contracts/WellKnownTypeRegistry.cs @@ -0,0 +1,26 @@ +namespace Elsa.Management.Contracts; + +public class WellKnownTypeRegistry : IWellKnownTypeRegistry +{ + private readonly IDictionary _aliasTypeDictionary = new Dictionary(); + private readonly IDictionary _typeAliasDictionary = new Dictionary(); + + public WellKnownTypeRegistry() + { + this.RegisterType("String"); + this.RegisterType("Int32"); + this.RegisterType("Int64"); + this.RegisterType("Decimal"); + this.RegisterType("Single"); + this.RegisterType("Double"); + } + + public void RegisterType(Type type, string alias) + { + _typeAliasDictionary[type] = alias; + _aliasTypeDictionary[alias] = type; + } + + public bool TryGetAlias(Type type, out string alias) => _typeAliasDictionary.TryGetValue(type, out alias!); + public bool TryGetType(string alias, out Type type) => _aliasTypeDictionary.TryGetValue(alias, out type!); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Elsa.Management.csproj b/src/core/Elsa.Management/Elsa.Management.csproj new file mode 100644 index 000000000..57c554a18 --- /dev/null +++ b/src/core/Elsa.Management/Elsa.Management.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/core/Elsa.Management/Extensions/ServiceCollectionExtensions.cs b/src/core/Elsa.Management/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..82267c3f8 --- /dev/null +++ b/src/core/Elsa.Management/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,41 @@ +using Elsa.Contracts; +using Elsa.Management.Contracts; +using Elsa.Management.Options; +using Elsa.Management.Providers; +using Elsa.Management.Serialization; +using Elsa.Management.Services; +using Elsa.Persistence.Mappers; +using Elsa.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Management.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddElsaManagement(this IServiceCollection services) + { + return services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } + + public static IServiceCollection ConfigureApiOptions(this IServiceCollection services, Action configure) => services.Configure(configure); + public static IServiceCollection AddActivity(this IServiceCollection services) where T:IActivity => services.ConfigureApiOptions(options => options.ActivityTypes.Add(typeof(T))); + public static IServiceCollection AddTrigger(this IServiceCollection services) where T:ITrigger => services.ConfigureApiOptions(options => options.TriggerTypes.Add(typeof(T))); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Extensions/TypeExtensions.cs b/src/core/Elsa.Management/Extensions/TypeExtensions.cs new file mode 100644 index 000000000..37cb5d798 --- /dev/null +++ b/src/core/Elsa.Management/Extensions/TypeExtensions.cs @@ -0,0 +1,8 @@ +namespace Elsa.Management.Extensions; + +public static class TypeExtensions +{ + public static bool IsGenericType(this Type type, Type genericType) => type.IsGenericType && type.GetGenericTypeDefinition() == genericType; + public static bool IsNullableType(this Type type) => type.IsGenericType(typeof(Nullable<>)); + public static Type GetTypeOfNullable(this Type type) => type.GenericTypeArguments[0]; +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/ActivityDescriptor.cs b/src/core/Elsa.Management/Models/ActivityDescriptor.cs new file mode 100644 index 000000000..16d2cd08f --- /dev/null +++ b/src/core/Elsa.Management/Models/ActivityDescriptor.cs @@ -0,0 +1,15 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Contracts; + +namespace Elsa.Management.Models; + +public class ActivityDescriptor : NodeDescriptor +{ + [JsonIgnore] public Func Constructor { get; init; } = default!; + public ActivityTraits Traits { get; set; } = ActivityTraits.Action; + public ICollection InPorts { get; init; } = new List(); + public ICollection OutPorts { get; init; } = new List(); +} + +public record ActivityConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions); \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/ActivityTraits.cs b/src/core/Elsa.Management/Models/ActivityTraits.cs new file mode 100644 index 000000000..a4111a00f --- /dev/null +++ b/src/core/Elsa.Management/Models/ActivityTraits.cs @@ -0,0 +1,8 @@ +namespace Elsa.Management.Models; + +[Flags] +public enum ActivityTraits +{ + Action = 1, + Trigger = 2 +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/ExpressionSyntaxDescriptor.cs b/src/core/Elsa.Management/Models/ExpressionSyntaxDescriptor.cs new file mode 100644 index 000000000..129dbfb37 --- /dev/null +++ b/src/core/Elsa.Management/Models/ExpressionSyntaxDescriptor.cs @@ -0,0 +1,26 @@ +using System.Text.Json; +using Elsa.Contracts; +using Elsa.Models; + +namespace Elsa.Management.Models; + +public class ExpressionSyntaxDescriptor +{ + public string Syntax { get; init; } = default!; + public Type Type { get; init; } = default!; + public Func CreateExpression { get; init; } = default!; + public Func CreateLocationReference { get; init; } = default!; + public Func CreateSerializableObject { get; init; } = default!; +} + +public record ExpressionConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions); + +public record LocationReferenceConstructorContext(IExpression Expression) +{ + public T GetExpression() => (T)Expression; +} + +public record SerializableObjectConstructorContext(IExpression Expression) +{ + public T GetExpression() => (T)Expression; +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/InputDescriptor.cs b/src/core/Elsa.Management/Models/InputDescriptor.cs new file mode 100644 index 000000000..ae68d3276 --- /dev/null +++ b/src/core/Elsa.Management/Models/InputDescriptor.cs @@ -0,0 +1,46 @@ +namespace Elsa.Management.Models; + +public class InputDescriptor : PropertyDescriptor +{ + public InputDescriptor() + { + } + + public InputDescriptor( + string name, + Type type, + string uiHint, + string displayName, + string? description = default, + object? options = default, + string? category = default, + float order = 0, + object? defaultValue = default, + string? defaultSyntax = "Literal", + IEnumerable? supportedSyntaxes = default, + bool isReadOnly = false, + bool isBrowsable = true) + { + Name = name; + Type = type; + UIHint = uiHint; + DisplayName = displayName; + Description = description; + Options = options; + Category = category; + Order = order; + DefaultValue = defaultValue; + DefaultSyntax = defaultSyntax; + SupportedSyntaxes = supportedSyntaxes?.ToList() ?? new List(); + IsReadOnly = isReadOnly; + IsBrowsable = isBrowsable; + } + + public string UIHint { get; set; } = default!; + public object? Options { get; set; } + public string? Category { get; set; } + public object? DefaultValue { get; set; } + public string? DefaultSyntax { get; set; } + public ICollection SupportedSyntaxes { get; set; } = new List(); + public bool? IsReadOnly { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/InputUIHints.cs b/src/core/Elsa.Management/Models/InputUIHints.cs new file mode 100644 index 000000000..0ed71854c --- /dev/null +++ b/src/core/Elsa.Management/Models/InputUIHints.cs @@ -0,0 +1,42 @@ +using System.Collections; +using Elsa.Attributes; +using Elsa.Management.Extensions; + +namespace Elsa.Management.Models; + +public static class InputUIHints +{ + public const string SingleLine = "single-line"; + public const string MultiLine = "multi-line"; + public const string Checkbox = "checkbox"; + public const string CheckList = "check-list"; + public const string RadioList = "radio-list"; + public const string Dropdown = "dropdown"; + public const string MultiText = "multi-text"; + public const string CodeEditor = "code-editor"; + + /// + /// An editor that allows the user to write a blob of JSON. + /// + public const string Json = "json"; + + public static string GetUIHint(Type wrappedPropertyType, InputAttribute? inputAttribute) + { + if (inputAttribute?.UIHint != null) + return inputAttribute.UIHint; + + if (wrappedPropertyType == typeof(bool) || wrappedPropertyType == typeof(bool?)) + return InputUIHints.Checkbox; + + if (wrappedPropertyType == typeof(string)) + return InputUIHints.SingleLine; + + if (typeof(IEnumerable).IsAssignableFrom(wrappedPropertyType)) + return InputUIHints.Dropdown; + + if (wrappedPropertyType.IsEnum || wrappedPropertyType.IsNullableType() && wrappedPropertyType.GetTypeOfNullable().IsEnum) + return InputUIHints.Dropdown; + + return InputUIHints.SingleLine; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/NodeDescriptor.cs b/src/core/Elsa.Management/Models/NodeDescriptor.cs new file mode 100644 index 000000000..b1372f4dd --- /dev/null +++ b/src/core/Elsa.Management/Models/NodeDescriptor.cs @@ -0,0 +1,11 @@ +namespace Elsa.Management.Models; + +public abstract class NodeDescriptor +{ + public string NodeType { get; init; } = default!; + public string Category { get; init; } = default!; + public string? DisplayName { get; init; } + public string? Description { get; init; } + public ICollection InputProperties { get; init; } = new List(); + public ICollection OutputProperties { get; init; } = new List(); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/OutputDescriptor.cs b/src/core/Elsa.Management/Models/OutputDescriptor.cs new file mode 100644 index 000000000..ff658c1b0 --- /dev/null +++ b/src/core/Elsa.Management/Models/OutputDescriptor.cs @@ -0,0 +1,18 @@ +namespace Elsa.Management.Models; + +public class OutputDescriptor : PropertyDescriptor +{ + public OutputDescriptor() + { + } + + public OutputDescriptor( + string name, + Type type, + string? description = default) + { + Name = name; + Type = type; + Description = description; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/Port.cs b/src/core/Elsa.Management/Models/Port.cs new file mode 100644 index 000000000..8af193d43 --- /dev/null +++ b/src/core/Elsa.Management/Models/Port.cs @@ -0,0 +1,7 @@ +namespace Elsa.Management.Models; + +public class Port +{ + public string Name { get; set; } = default!; + public string? DisplayName { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/PropertyDescriptor.cs b/src/core/Elsa.Management/Models/PropertyDescriptor.cs new file mode 100644 index 000000000..fc5e5aadc --- /dev/null +++ b/src/core/Elsa.Management/Models/PropertyDescriptor.cs @@ -0,0 +1,11 @@ +namespace Elsa.Management.Models; + +public abstract class PropertyDescriptor +{ + public string Name { get; set; } = default!; + public Type Type { get; set; } = default!; + public string? DisplayName { get; set; } + public string? Description { get; set; } + public float Order { get; set; } + public bool? IsBrowsable { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/SelectListItem.cs b/src/core/Elsa.Management/Models/SelectListItem.cs new file mode 100644 index 000000000..20a5494fb --- /dev/null +++ b/src/core/Elsa.Management/Models/SelectListItem.cs @@ -0,0 +1,3 @@ +namespace Elsa.Management.Models; + +public record SelectListItem(string Text, string Value); \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/TriggerDescriptor.cs b/src/core/Elsa.Management/Models/TriggerDescriptor.cs new file mode 100644 index 000000000..7be283e6d --- /dev/null +++ b/src/core/Elsa.Management/Models/TriggerDescriptor.cs @@ -0,0 +1,12 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Contracts; + +namespace Elsa.Management.Models; + +public class TriggerDescriptor : NodeDescriptor +{ + [JsonIgnore] public Func Constructor { get; init; } = default!; +} + +public record TriggerConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions); \ No newline at end of file diff --git a/src/core/Elsa.Management/Notifications/WorkflowPublished.cs b/src/core/Elsa.Management/Notifications/WorkflowPublished.cs new file mode 100644 index 000000000..5bcee7c48 --- /dev/null +++ b/src/core/Elsa.Management/Notifications/WorkflowPublished.cs @@ -0,0 +1,5 @@ +using Elsa.Mediator.Contracts; +using Elsa.Models; + +namespace Elsa.Management.Notifications; +public record WorkflowPublished(Workflow Workflow) : INotification; \ No newline at end of file diff --git a/src/core/Elsa.Management/Notifications/WorkflowPublishing.cs b/src/core/Elsa.Management/Notifications/WorkflowPublishing.cs new file mode 100644 index 000000000..bdbe263ca --- /dev/null +++ b/src/core/Elsa.Management/Notifications/WorkflowPublishing.cs @@ -0,0 +1,6 @@ +using Elsa.Mediator.Contracts; +using Elsa.Models; + +namespace Elsa.Management.Notifications; + +public record WorkflowPublishing(Workflow Workflow) : INotification; \ No newline at end of file diff --git a/src/core/Elsa.Management/Notifications/WorkflowRetracted.cs b/src/core/Elsa.Management/Notifications/WorkflowRetracted.cs new file mode 100644 index 000000000..704281549 --- /dev/null +++ b/src/core/Elsa.Management/Notifications/WorkflowRetracted.cs @@ -0,0 +1,6 @@ +using Elsa.Mediator.Contracts; +using Elsa.Models; + +namespace Elsa.Management.Notifications; + +public record WorkflowRetracted(Workflow Workflow) : INotification; \ No newline at end of file diff --git a/src/core/Elsa.Management/Notifications/WorkflowRetracting.cs b/src/core/Elsa.Management/Notifications/WorkflowRetracting.cs new file mode 100644 index 000000000..ef842b146 --- /dev/null +++ b/src/core/Elsa.Management/Notifications/WorkflowRetracting.cs @@ -0,0 +1,6 @@ +using Elsa.Mediator.Contracts; +using Elsa.Models; + +namespace Elsa.Management.Notifications; + +public record WorkflowRetracting(Workflow Workflow) : INotification; \ No newline at end of file diff --git a/src/core/Elsa.Management/Options/ApiOptions.cs b/src/core/Elsa.Management/Options/ApiOptions.cs new file mode 100644 index 000000000..fbaef310b --- /dev/null +++ b/src/core/Elsa.Management/Options/ApiOptions.cs @@ -0,0 +1,14 @@ +namespace Elsa.Management.Options; + +public class ApiOptions +{ + /// + /// A collection of activity types made available from the API. + /// + public HashSet ActivityTypes { get; set; } = new(); + + /// + /// A collection of trigger types made available from the API. + /// + public HashSet TriggerTypes { get; set; } = new(); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Providers/DefaultExpressionSyntaxProvider.cs b/src/core/Elsa.Management/Providers/DefaultExpressionSyntaxProvider.cs new file mode 100644 index 000000000..ddb15cd51 --- /dev/null +++ b/src/core/Elsa.Management/Providers/DefaultExpressionSyntaxProvider.cs @@ -0,0 +1,77 @@ +using Elsa.Contracts; +using Elsa.Expressions; +using Elsa.Management.Contracts; +using Elsa.Management.Models; +using Elsa.Models; + +namespace Elsa.Management.Providers; + +public class DefaultExpressionSyntaxProvider : IExpressionSyntaxProvider +{ + private readonly IIdentityGenerator _identityGenerator; + + public DefaultExpressionSyntaxProvider(IIdentityGenerator identityGenerator) + { + _identityGenerator = identityGenerator; + } + + public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var literal = CreateLiteralDescriptor(); + var json = CreateJsonDescriptor(); + + return ValueTask.FromResult>(new[] { literal, json }); + } + + private ExpressionSyntaxDescriptor CreateLiteralDescriptor() => CreateDescriptor( + "Literal", + CreateLiteralExpression, + context => new Literal(context.GetExpression().Value), + expression => expression.Value); + + private ExpressionSyntaxDescriptor CreateJsonDescriptor() => CreateDescriptor( + "Json", + CreateJsonExpression, + context => new JsonObject(context.GetExpression().Value), + expression => expression.Value); + + private ExpressionSyntaxDescriptor CreateDescriptor( + string syntax, + Func constructor, + Func createLocationReference, + Func expressionValue) => + new() + { + Syntax = syntax, + Type = typeof(TExpression), + CreateExpression = constructor, + CreateLocationReference = context => + { + var reference = createLocationReference(context); + + if (string.IsNullOrWhiteSpace(reference.Id)) + reference.Id = GenerateId(); + + return reference; + }, + CreateSerializableObject = context => new + { + Type = syntax, + Value = expressionValue(context.GetExpression()) + } + }; + + private IExpression CreateLiteralExpression(ExpressionConstructorContext context) + { + var expressionValue = context.Element.GetProperty("value").ToString(); + return new LiteralExpression(expressionValue); + } + + private IExpression CreateJsonExpression(ExpressionConstructorContext context) + { + var expressionValue = context.Element.GetProperty("value").ToString(); + return new JsonExpression(expressionValue); + } + + private string GenerateId() => _identityGenerator.GenerateId(); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Providers/TypedActivityProvider.cs b/src/core/Elsa.Management/Providers/TypedActivityProvider.cs new file mode 100644 index 000000000..46ada1fb2 --- /dev/null +++ b/src/core/Elsa.Management/Providers/TypedActivityProvider.cs @@ -0,0 +1,35 @@ +using System.Runtime.CompilerServices; +using Elsa.Management.Contracts; +using Elsa.Management.Models; +using Elsa.Management.Options; +using Microsoft.Extensions.Options; + +namespace Elsa.Management.Providers; + +public class TypedActivityProvider : IActivityProvider +{ + private readonly IActivityDescriber _activityDescriber; + private readonly ApiOptions _options; + + public TypedActivityProvider(IOptions options, IActivityDescriber activityDescriber) + { + _activityDescriber = activityDescriber; + _options = options.Value; + } + + public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var activityTypes = _options.ActivityTypes; + var descriptors = await DescribeActivityTypesAsync(activityTypes, cancellationToken).ToListAsync(cancellationToken); + return descriptors; + } + + public async IAsyncEnumerable DescribeActivityTypesAsync(IEnumerable activityTypes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (var activityType in activityTypes) + { + var descriptor = await _activityDescriber.DescribeActivityAsync(activityType, cancellationToken); + yield return descriptor; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Providers/TypedTriggerProvider.cs b/src/core/Elsa.Management/Providers/TypedTriggerProvider.cs new file mode 100644 index 000000000..7c9846fc4 --- /dev/null +++ b/src/core/Elsa.Management/Providers/TypedTriggerProvider.cs @@ -0,0 +1,35 @@ +using System.Runtime.CompilerServices; +using Elsa.Management.Contracts; +using Elsa.Management.Models; +using Elsa.Management.Options; +using Microsoft.Extensions.Options; + +namespace Elsa.Management.Providers; + +public class TypedTriggerProvider : ITriggerProvider +{ + private readonly ITriggerDescriber _triggerDescriber; + private readonly ApiOptions _options; + + public TypedTriggerProvider(IOptions options, ITriggerDescriber triggerDescriber) + { + _triggerDescriber = triggerDescriber; + _options = options.Value; + } + + public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var triggerTypes = _options.TriggerTypes; + var descriptors = await DescribeTriggerTypesAsync(triggerTypes, cancellationToken).ToListAsync(cancellationToken); + return descriptors; + } + + public async IAsyncEnumerable DescribeTriggerTypesAsync(IEnumerable triggerTypes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (var triggerType in triggerTypes) + { + var descriptor = await _triggerDescriber.DescribeTriggerAsync(triggerType, cancellationToken); + yield return descriptor; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverter.cs new file mode 100644 index 000000000..eccbdfc7c --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverter.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Contracts; +using Elsa.Management.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Serialization.Converters; + +/// +/// (De)serializes objects of type . +/// +public class ActivityJsonConverter : JsonConverter +{ + private readonly IActivityRegistry _activityRegistry; + private readonly IServiceProvider _serviceProvider; + + public ActivityJsonConverter(IActivityRegistry activityRegistry, IServiceProvider serviceProvider) + { + _activityRegistry = activityRegistry; + _serviceProvider = serviceProvider; + } + + public override IActivity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + throw new JsonException("Failed to parse JsonDocument"); + + if (!doc.RootElement.TryGetProperty("nodeType", out var activityTypeNameElement)) + throw new JsonException("Failed to extract activity type property"); + + var activityTypeName = activityTypeNameElement.GetString()!; + var activityDescriptor = _activityRegistry.Find(activityTypeName); + + if (activityDescriptor == null) + throw new Exception($"Activity of type {activityTypeName} not found in registry"); + + var newOptions = new JsonSerializerOptions(options); + newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); + + var context = new ActivityConstructorContext(doc.RootElement, newOptions); + var activity = activityDescriptor.Constructor(context); + + return activity; + } + + public override void Write(Utf8JsonWriter writer, IActivity value, JsonSerializerOptions options) + { + var newOptions = new JsonSerializerOptions(options); + newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); + JsonSerializer.Serialize(writer, value, value.GetType(), newOptions); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverterFactory.cs b/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverterFactory.cs new file mode 100644 index 000000000..de29c0253 --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverterFactory.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Contracts; +using Elsa.Management.Contracts; + +namespace Elsa.Management.Serialization.Converters; + +public class ActivityJsonConverterFactory : JsonConverterFactory +{ + private readonly IActivityRegistry _activityRegistry; + private readonly IServiceProvider _serviceProvider; + + public ActivityJsonConverterFactory(IActivityRegistry activityRegistry, IServiceProvider serviceProvider) + { + _activityRegistry = activityRegistry; + _serviceProvider = serviceProvider; + } + + // Notice that this factory only creates converters when the type to convert is IActivity. + // The ActivityJsonConverter will create concrete activity objects, which then uses regular serialization + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(IActivity); + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new ActivityJsonConverter(_activityRegistry, _serviceProvider); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/ConnectionJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/ConnectionJsonConverter.cs new file mode 100644 index 000000000..c0b64eabc --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/ConnectionJsonConverter.cs @@ -0,0 +1,47 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Activities.Workflows; +using Elsa.Contracts; + +namespace Elsa.Management.Serialization.Converters; + +public class ConnectionJsonConverter : JsonConverter +{ + private readonly IDictionary _activities; + + public ConnectionJsonConverter(IDictionary activities) + { + _activities = activities; + } + + public override Connection? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + throw new JsonException("Failed to parse JsonDocument"); + + var sourceId = doc.RootElement.GetProperty("source").GetString()!; + var targetId = doc.RootElement.GetProperty("target").GetString()!; + var sourcePort = doc.RootElement.GetProperty("sourcePort").GetString()!; + var targetPort = doc.RootElement.GetProperty("targetPort").GetString()!; + + var source = _activities[sourceId]; + var target = _activities[targetId]; + + return new Connection(source, target, sourcePort, targetPort); + } + + public override void Write(Utf8JsonWriter writer, Connection value, JsonSerializerOptions options) + { + var (activity, target, sourcePort, targetPort) = value; + + var model = new + { + Source = activity.Id, + Target = target.Id, + SourcePort = sourcePort, + TargetPort = targetPort + }; + + JsonSerializer.Serialize(writer, model, options); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/FlowchartJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/FlowchartJsonConverter.cs new file mode 100644 index 000000000..51a107f3f --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/FlowchartJsonConverter.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Activities.Workflows; +using Elsa.Contracts; +using Elsa.Extensions; + +namespace Elsa.Management.Serialization.Converters; + +public class FlowchartJsonConverter : JsonConverter +{ + private readonly IActivityWalker _activityWalker; + + public FlowchartJsonConverter(IActivityWalker activityWalker) + { + _activityWalker = activityWalker; + } + + public override Flowchart? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + throw new JsonException("Failed to parse JsonDocument"); + + var connectionsElement = doc.RootElement.GetProperty("connections"); + var metadataElement = doc.RootElement.GetProperty("metadata"); + var activitiesElement = doc.RootElement.GetProperty("activities"); + var id = doc.RootElement.GetProperty("id").GetString()!; + var startId = doc.RootElement.TryGetProperty("start", out var startElement) ? startElement.GetString() : default; + var rootActivities = activitiesElement.Deserialize(options) ?? Array.Empty(); + var metadata = metadataElement.Deserialize>(options) ?? new Dictionary(); + var start = rootActivities.FirstOrDefault(x => x.Id == startId) ?? rootActivities.FirstOrDefault(); + var connectionSerializerOptions = new JsonSerializerOptions(options); + var activities = WalkActivityTree(rootActivities).ToList(); + var activityDictionary = activities.ToDictionary(x => x.Id); + connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); + + var connections = connectionsElement.Deserialize(connectionSerializerOptions) ?? Array.Empty(); + + return new Flowchart + { + Id = id, + Metadata = metadata, + Activities = rootActivities, + Connections = connections, + Start = start, + }; + } + + public override void Write(Utf8JsonWriter writer, Flowchart value, JsonSerializerOptions options) + { + var activities = value.Activities; + var connectionSerializerOptions = new JsonSerializerOptions(options); + var activityDictionary = activities.ToDictionary(x => x.Id); + connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); + + var model = new + { + NodeType = value.NodeType, + Id = value.Id, + Metadata = value.Metadata, + Start = value.Start?.Id, + Activities = value.Activities, + Connections = value.Connections + }; + + JsonSerializer.Serialize(writer, model, connectionSerializerOptions); + } + + private IEnumerable WalkActivityTree(IEnumerable rootActivities) => rootActivities + .Select(rootActivity => _activityWalker.Walk(rootActivity)) + .SelectMany(node => node.Flatten().Select(x => x.Activity)); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/InputJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/InputJsonConverter.cs new file mode 100644 index 000000000..1b941f18c --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/InputJsonConverter.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Management.Contracts; +using Elsa.Management.Models; +using Elsa.Models; + +namespace Elsa.Management.Serialization.Converters; + +/// +/// Serializes objects. +/// +public class InputJsonConverter : JsonConverter> +{ + private readonly IExpressionSyntaxRegistry _expressionSyntaxRegistry; + + public InputJsonConverter(IExpressionSyntaxRegistry expressionSyntaxRegistry) + { + _expressionSyntaxRegistry = expressionSyntaxRegistry; + } + + public override bool CanConvert(Type typeToConvert) => typeof(Input).IsAssignableFrom(typeToConvert); + + public override Input Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + throw new JsonException("Failed to parse JsonDocument"); + + if (!doc.RootElement.TryGetProperty("type", out var inputTargetTypeElement)) + throw new JsonException("Failed to extract input type property"); + + var expressionElement = doc.RootElement.GetProperty("expression"); + + if (!expressionElement.TryGetProperty("type", out var expressionTypeNameElement)) + throw new JsonException("Failed to extract expression type property"); + + var expressionTypeName = expressionTypeNameElement.GetString()!; + var expressionSyntaxDescriptor = _expressionSyntaxRegistry.Find(expressionTypeName); + + if (expressionSyntaxDescriptor == null) + throw new Exception($"Syntax with name {expressionTypeName} not found in registry"); + + var context = new ExpressionConstructorContext(expressionElement, options); + var expression = expressionSyntaxDescriptor.CreateExpression(context); + var locationReference = expressionSyntaxDescriptor.CreateLocationReference(new LocationReferenceConstructorContext(expression)); + + return (Input)Activator.CreateInstance(typeof(Input), expression, locationReference)!; + } + + public override void Write(Utf8JsonWriter writer, Input value, JsonSerializerOptions options) + { + var expression = value.Expression; + var expressionType = expression.GetType(); + var targetType = value.Type; + var expressionSyntaxDescriptor = _expressionSyntaxRegistry.Find(x => x.Type == expressionType); + + if (expressionSyntaxDescriptor == null) + throw new Exception($"Syntax descriptor with expression type {expressionType} not found in registry"); + + var model = new + { + Type = targetType, + Expression = expressionSyntaxDescriptor.CreateSerializableObject(new SerializableObjectConstructorContext(expression)) + }; + + JsonSerializer.Serialize(writer, model, options); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/InputJsonConverterFactory.cs b/src/core/Elsa.Management/Serialization/Converters/InputJsonConverterFactory.cs new file mode 100644 index 000000000..57588957a --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/InputJsonConverterFactory.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Management.Serialization.Converters; + +public class InputJsonConverterFactory : JsonConverterFactory +{ + private readonly IServiceProvider _serviceProvider; + public InputJsonConverterFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public override bool CanConvert(Type typeToConvert) => typeof(Input).IsAssignableFrom(typeToConvert); + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var type = typeToConvert.GetGenericArguments().First(); + return (JsonConverter)ActivatorUtilities.CreateInstance(_serviceProvider, typeof(InputJsonConverter<>).MakeGenericType(type))!; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/TriggerJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/TriggerJsonConverter.cs new file mode 100644 index 000000000..02fc58cd5 --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/TriggerJsonConverter.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Contracts; +using Elsa.Management.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Serialization.Converters; + +/// +/// (De)serializes objects of type . +/// +public class TriggerJsonConverter : JsonConverter +{ + private readonly ITriggerRegistry _triggerRegistry; + private readonly IServiceProvider _serviceProvider; + + public TriggerJsonConverter(ITriggerRegistry triggerRegistry, IServiceProvider serviceProvider) + { + _triggerRegistry = triggerRegistry; + _serviceProvider = serviceProvider; + } + + public override ITrigger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + throw new JsonException("Failed to parse JsonDocument"); + + if (!doc.RootElement.TryGetProperty("nodeType", out var triggerTypeNameElement)) + throw new JsonException("Failed to extract trigger type property"); + + var triggerType = triggerTypeNameElement.GetString()!; + var triggerDescriptor = _triggerRegistry.Find(triggerType); + + if (triggerDescriptor == null) + throw new Exception($"Trigger of type {triggerType} not found in registry"); + + var newOptions = new JsonSerializerOptions(options); + newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); + + var context = new TriggerConstructorContext(doc.RootElement, newOptions); + var activity = triggerDescriptor.Constructor(context); + + return activity; + } + + public override void Write(Utf8JsonWriter writer, ITrigger value, JsonSerializerOptions options) + { + var newOptions = new JsonSerializerOptions(options); + newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); + JsonSerializer.Serialize(writer, value, value.GetType(), newOptions); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/TriggerJsonConverterFactory.cs b/src/core/Elsa.Management/Serialization/Converters/TriggerJsonConverterFactory.cs new file mode 100644 index 000000000..26f0e396a --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/TriggerJsonConverterFactory.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Contracts; +using Elsa.Management.Contracts; + +namespace Elsa.Management.Serialization.Converters; + +public class TriggerJsonConverterFactory : JsonConverterFactory +{ + private readonly ITriggerRegistry _triggerRegistry; + private readonly IServiceProvider _serviceProvider; + + public TriggerJsonConverterFactory(ITriggerRegistry triggerRegistry, IServiceProvider serviceProvider) + { + _triggerRegistry = triggerRegistry; + _serviceProvider = serviceProvider; + } + + // Notice that this factory only creates converters when the type to convert is ITrigger. + // The TriggerJsonConverter will create concrete trigger objects, which then uses regular serialization + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ITrigger); + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new TriggerJsonConverter(_triggerRegistry, _serviceProvider); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/TypeJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/TypeJsonConverter.cs new file mode 100644 index 000000000..49b12ae6a --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/TypeJsonConverter.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Management.Contracts; + +namespace Elsa.Management.Serialization.Converters; + +/// +/// Serializes objects to a simple alias representing said type. +/// +public class TypeJsonConverter : JsonConverter +{ + private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry; + + public TypeJsonConverter(IWellKnownTypeRegistry wellKnownTypeRegistry) + { + _wellKnownTypeRegistry = wellKnownTypeRegistry; + } + + public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var typeAlias = reader.GetString()!; + + // Handle collection types. + if (typeAlias.EndsWith("[]")) + { + var elementTypeAlias = typeAlias[..^"[]".Length]; + var elementType = _wellKnownTypeRegistry.TryGetType(typeAlias, out var t) ? t : Type.GetType(elementTypeAlias)!; + return typeof(List<>).MakeGenericType(elementType); + } + + return _wellKnownTypeRegistry.TryGetType(typeAlias, out var type) ? type : Type.GetType(typeAlias); + } + + public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) + { + // Handle collection types. + if (value.IsGenericType) + { + var elementType = value.GenericTypeArguments.First(); + var typedEnumerable = typeof(IEnumerable<>).MakeGenericType(elementType); + + if (typedEnumerable.IsAssignableFrom(value)) + { + var elementTypeAlias = _wellKnownTypeRegistry.TryGetAlias(elementType, out var elementAlias) ? elementAlias : value.AssemblyQualifiedName; + JsonSerializer.Serialize(writer, $"{elementTypeAlias}[]", options); + return; + } + } + + var typeAlias = _wellKnownTypeRegistry.TryGetAlias(value, out var @alias) ? alias : value.AssemblyQualifiedName; + JsonSerializer.Serialize(writer, typeAlias, options); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Extensions/JsonSerializerExtensions.cs b/src/core/Elsa.Management/Serialization/Extensions/JsonSerializerExtensions.cs new file mode 100644 index 000000000..56c13e646 --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Extensions/JsonSerializerExtensions.cs @@ -0,0 +1,12 @@ +using System.Text.Json; + +namespace Elsa.Management.Serialization.Extensions; + +public static class JsonSerializerExtensions +{ + public static T? DeserializeAnonymousType(string json, T anonymousTypeObject, JsonSerializerOptions? options = default) + => JsonSerializer.Deserialize(json, options); + + public static ValueTask DeserializeAnonymousTypeAsync(Stream stream, TValue anonymousTypeObject, JsonSerializerOptions? options = default, CancellationToken cancellationToken = default) + => JsonSerializer.DeserializeAsync(stream, options, cancellationToken); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/WorkflowSerializerOptionsProvider.cs b/src/core/Elsa.Management/Serialization/WorkflowSerializerOptionsProvider.cs new file mode 100644 index 000000000..af74b86be --- /dev/null +++ b/src/core/Elsa.Management/Serialization/WorkflowSerializerOptionsProvider.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Management.Serialization.Converters; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Management.Serialization; + +public class WorkflowSerializerOptionsProvider +{ + private readonly IServiceProvider _serviceProvider; + public WorkflowSerializerOptionsProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + public JsonSerializerOptions CreateSerializerOptions() => new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + Create(), + Create(), + Create(), + Create(), + Create() + } + }; + + private T Create() => ActivatorUtilities.CreateInstance(_serviceProvider); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/ActivityDescriber.cs b/src/core/Elsa.Management/Services/ActivityDescriber.cs new file mode 100644 index 000000000..e3793f934 --- /dev/null +++ b/src/core/Elsa.Management/Services/ActivityDescriber.cs @@ -0,0 +1,139 @@ +using System.Collections; +using System.ComponentModel; +using System.Reflection; +using Elsa.Attributes; +using Elsa.Contracts; +using Elsa.Helpers; +using Elsa.Management.Contracts; +using Elsa.Management.Extensions; +using Elsa.Management.Models; +using Elsa.Models; +using Humanizer; + +namespace Elsa.Management.Services; + +public class ActivityDescriber : IActivityDescriber +{ + private readonly IPropertyOptionsResolver _optionsResolver; + private readonly IPropertyDefaultValueResolver _defaultValueResolver; + private readonly IActivityFactory _activityFactory; + + public ActivityDescriber(IPropertyOptionsResolver optionsResolver, IPropertyDefaultValueResolver defaultValueResolver, IActivityFactory activityFactory) + { + _optionsResolver = optionsResolver; + _defaultValueResolver = defaultValueResolver; + _activityFactory = activityFactory; + } + + public ValueTask DescribeActivityAsync(Type activityType, CancellationToken cancellationToken = default) + { + var ns = TypeNameHelper.GenerateNamespace(activityType); + var typeName = activityType.Name; + var fullTypeName = TypeNameHelper.GenerateTypeName(activityType, ns); + var displayNameAttr = activityType.GetCustomAttribute(); + var displayName = displayNameAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title); + var categoryAttr = activityType.GetCustomAttribute(); + var category = categoryAttr?.Category ?? TypeNameHelper.GetCategoryFromNamespace(ns) ?? "Miscellaneous"; + var descriptionAttr = activityType.GetCustomAttribute(); + var description = descriptionAttr?.Description; + + var outboundPorts = + from prop in activityType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + where typeof(IActivity).IsAssignableFrom(prop.PropertyType) || typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) + let portAttr = prop.GetCustomAttribute() + where portAttr != null + select new Port + { + Name = portAttr.Name ?? prop.Name, + DisplayName = portAttr.DisplayName ?? portAttr.Name ?? prop.Name + }; + + var properties = activityType.GetProperties(); + var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType)).ToList(); + var outputProperties = properties.Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).ToList(); + var isTrigger = activityType.IsAssignableTo(typeof(ITrigger)); + + var descriptor = new ActivityDescriptor + { + Category = category, + Description = description, + NodeType = fullTypeName, + DisplayName = displayName, + Traits = isTrigger ? ActivityTraits.Trigger : ActivityTraits.Action, + OutPorts = outboundPorts.ToList(), + InputProperties = DescribeInputProperties(inputProperties).ToList(), + OutputProperties = DescribeOutputProperties(outputProperties).ToList(), + Constructor = context => + { + var activity = _activityFactory.Create(activityType, context); + activity.NodeType = fullTypeName; + return activity; + } + }; + + return ValueTask.FromResult(descriptor); + } + + private IEnumerable DescribeInputProperties(IEnumerable properties) + { + foreach (var propertyInfo in properties) + { + var inputAttribute = propertyInfo.GetCustomAttribute(); + var descriptionAttribute = propertyInfo.GetCustomAttribute(); + var wrappedPropertyType = propertyInfo.PropertyType.GenericTypeArguments[0]; + + yield return new InputDescriptor + ( + inputAttribute?.Name ?? propertyInfo.Name, + wrappedPropertyType, + GetUIHint(wrappedPropertyType, inputAttribute), + inputAttribute?.DisplayName ?? propertyInfo.Name.Humanize(LetterCasing.Title), + descriptionAttribute?.Description, + _optionsResolver.GetOptions(propertyInfo), + inputAttribute?.Category, + inputAttribute?.Order ?? 0, + _defaultValueResolver.GetDefaultValue(propertyInfo), + inputAttribute?.DefaultSyntax, + inputAttribute?.SupportedSyntaxes, + inputAttribute?.IsReadOnly ?? false, + inputAttribute?.IsBrowsable ?? true + ); + } + } + + private IEnumerable DescribeOutputProperties(IEnumerable properties) + { + foreach (var propertyInfo in properties) + { + var activityPropertyAttribute = propertyInfo.GetCustomAttribute(); + var wrappedPropertyType = propertyInfo.PropertyType.GenericTypeArguments[0]; + + yield return new OutputDescriptor + ( + (activityPropertyAttribute?.Name ?? propertyInfo.Name).Pascalize(), + wrappedPropertyType, + activityPropertyAttribute?.Description + ); + } + } + + private string GetUIHint(Type wrappedPropertyType, InputAttribute? inputAttribute) + { + if (inputAttribute?.UIHint != null) + return inputAttribute.UIHint; + + if (wrappedPropertyType == typeof(bool) || wrappedPropertyType == typeof(bool?)) + return InputUIHints.Checkbox; + + if (wrappedPropertyType == typeof(string)) + return InputUIHints.SingleLine; + + if (typeof(IEnumerable).IsAssignableFrom(wrappedPropertyType)) + return InputUIHints.Dropdown; + + if (wrappedPropertyType.IsEnum || wrappedPropertyType.IsNullableType() && wrappedPropertyType.GetTypeOfNullable().IsEnum) + return InputUIHints.Dropdown; + + return InputUIHints.SingleLine; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/ActivityFactory.cs b/src/core/Elsa.Management/Services/ActivityFactory.cs new file mode 100644 index 000000000..e74463cdf --- /dev/null +++ b/src/core/Elsa.Management/Services/ActivityFactory.cs @@ -0,0 +1,15 @@ +using System.Text.Json; +using Elsa.Contracts; +using Elsa.Management.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Services; + +public class ActivityFactory : IActivityFactory +{ + public IActivity Create(Type type, ActivityConstructorContext context) + { + var activity = (IActivity)context.Element.Deserialize(type, context.SerializerOptions)!; + return activity; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/ActivityRegistry.cs b/src/core/Elsa.Management/Services/ActivityRegistry.cs new file mode 100644 index 000000000..3ce53ba3d --- /dev/null +++ b/src/core/Elsa.Management/Services/ActivityRegistry.cs @@ -0,0 +1,58 @@ +using Elsa.Management.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Services; + +public class ActivityRegistry : IActivityRegistry +{ + private readonly IDictionary> _providedActivityDescriptors = new Dictionary>(); + private readonly IDictionary _activityDescriptors = new Dictionary(); + + public void Add(object provider, ActivityDescriptor descriptor) => Add(descriptor, GetOrCreateDescriptors(provider)); + + public void AddMany(object provider, IEnumerable descriptors) + { + var target = GetOrCreateDescriptors(provider); + + foreach (var descriptor in descriptors) + Add(descriptor, target); + } + + public void Clear() + { + _activityDescriptors.Clear(); + _providedActivityDescriptors.Clear(); + } + + public void ClearProvider(object provider) + { + var descriptors = ListByProvider(provider).ToList(); + + foreach (var descriptor in descriptors) + _activityDescriptors.Remove(descriptor.NodeType); + + _providedActivityDescriptors.Remove(provider); + } + + public IEnumerable ListAll() => _activityDescriptors.Values; + public IEnumerable ListByProvider(object provider) => _providedActivityDescriptors.TryGetValue(provider, out var descriptors) ? descriptors : ArraySegment.Empty; + public ActivityDescriptor? Find(Func predicate) => _activityDescriptors.Values.FirstOrDefault(predicate); + public ActivityDescriptor? Find(string activityType) => _activityDescriptors.TryGetValue(activityType, out var descriptor) ? descriptor : null; + + private void Add(ActivityDescriptor descriptor, ICollection target) + { + _activityDescriptors.Add(descriptor.NodeType, descriptor); + target.Add(descriptor); + } + + private ICollection GetOrCreateDescriptors(object provider) + { + if (_providedActivityDescriptors.TryGetValue(provider, out var descriptors)) + return descriptors; + + descriptors = new List(); + _providedActivityDescriptors.Add(provider, descriptors); + + return descriptors; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/ActivityRegistryPopulator.cs b/src/core/Elsa.Management/Services/ActivityRegistryPopulator.cs new file mode 100644 index 000000000..c5ecbb207 --- /dev/null +++ b/src/core/Elsa.Management/Services/ActivityRegistryPopulator.cs @@ -0,0 +1,27 @@ +using Elsa.Management.Contracts; + +namespace Elsa.Management.Services; + +/// +/// Populates the with descriptors provided by s. +/// +public class ActivityRegistryPopulator : IActivityRegistryPopulator +{ + private readonly IEnumerable _providers; + private readonly IActivityRegistry _registry; + + public ActivityRegistryPopulator(IEnumerable providers, IActivityRegistry registry) + { + _providers = providers; + _registry = registry; + } + + public async ValueTask PopulateRegistryAsync(CancellationToken cancellationToken) + { + foreach (var provider in _providers) + { + var descriptors = await provider.GetDescriptorsAsync(cancellationToken); + _registry.AddMany(provider, descriptors); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/ExpressionSyntaxRegistry.cs b/src/core/Elsa.Management/Services/ExpressionSyntaxRegistry.cs new file mode 100644 index 000000000..b18295bc7 --- /dev/null +++ b/src/core/Elsa.Management/Services/ExpressionSyntaxRegistry.cs @@ -0,0 +1,20 @@ +using Elsa.Management.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Services; + +public class ExpressionSyntaxRegistry : IExpressionSyntaxRegistry +{ + private readonly IDictionary _expressionSyntaxDescriptors = new Dictionary(); + public void Add(ExpressionSyntaxDescriptor descriptor) => _expressionSyntaxDescriptors[descriptor.Syntax] = descriptor; + + public void AddMany(IEnumerable descriptors) + { + foreach (var descriptor in descriptors) + Add(descriptor); + } + + public IEnumerable ListAll() => _expressionSyntaxDescriptors.Values; + public ExpressionSyntaxDescriptor? Find(Func predicate) => _expressionSyntaxDescriptors.Values.FirstOrDefault(predicate); + public ExpressionSyntaxDescriptor? Find(string syntax) => Find(x => x.Syntax == syntax); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/ExpressionSyntaxRegistryPopulator.cs b/src/core/Elsa.Management/Services/ExpressionSyntaxRegistryPopulator.cs new file mode 100644 index 000000000..9e79a046f --- /dev/null +++ b/src/core/Elsa.Management/Services/ExpressionSyntaxRegistryPopulator.cs @@ -0,0 +1,24 @@ +using Elsa.Management.Contracts; + +namespace Elsa.Management.Services; + +public class ExpressionSyntaxRegistryPopulator : IExpressionSyntaxRegistryPopulator +{ + private readonly IEnumerable _providers; + private readonly IExpressionSyntaxRegistry _registry; + + public ExpressionSyntaxRegistryPopulator(IEnumerable providers, IExpressionSyntaxRegistry registry) + { + _providers = providers; + _registry = registry; + } + + public async ValueTask PopulateRegistryAsync(CancellationToken cancellationToken) + { + foreach (var provider in _providers) + { + var descriptors = await provider.GetDescriptorsAsync(cancellationToken); + _registry.AddMany(descriptors); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/IActivityPropertyOptionsProvider.cs b/src/core/Elsa.Management/Services/IActivityPropertyOptionsProvider.cs new file mode 100644 index 000000000..0e9b48a68 --- /dev/null +++ b/src/core/Elsa.Management/Services/IActivityPropertyOptionsProvider.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Elsa.Management.Services; + +public interface IActivityPropertyOptionsProvider +{ + object? GetOptions(PropertyInfo property); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/PropertyDefaultValueResolver.cs b/src/core/Elsa.Management/Services/PropertyDefaultValueResolver.cs new file mode 100644 index 000000000..09c6ec180 --- /dev/null +++ b/src/core/Elsa.Management/Services/PropertyDefaultValueResolver.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using Elsa.Attributes; +using Elsa.Management.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Management.Services; + +public class PropertyDefaultValueResolver : IPropertyDefaultValueResolver +{ + private readonly IServiceProvider _serviceProvider; + + public PropertyDefaultValueResolver(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public object? GetDefaultValue(PropertyInfo activityPropertyInfo) + { + var inputAttribute = activityPropertyInfo.GetCustomAttribute(); + + if (inputAttribute == null) + return null; + + if (inputAttribute.DefaultValueProvider == null) + return inputAttribute.DefaultValue; + + var providerType = inputAttribute.DefaultValueProvider; + + using var scope = _serviceProvider.CreateScope(); + var provider = (IActivityPropertyDefaultValueProvider) ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, providerType); + return provider.GetDefaultValue(activityPropertyInfo); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/PropertyOptionsResolver.cs b/src/core/Elsa.Management/Services/PropertyOptionsResolver.cs new file mode 100644 index 000000000..bf6187db7 --- /dev/null +++ b/src/core/Elsa.Management/Services/PropertyOptionsResolver.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using Elsa.Attributes; +using Elsa.Management.Contracts; +using Elsa.Management.Extensions; +using Elsa.Management.Models; +using Humanizer; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Management.Services; + +public class PropertyOptionsResolver : IPropertyOptionsResolver +{ + private readonly IServiceProvider _serviceProvider; + + public PropertyOptionsResolver(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public object? GetOptions(PropertyInfo propertyInfo) + { + var inputAttribute = propertyInfo.GetCustomAttribute(); + + if (inputAttribute == null) + return null; + + if (inputAttribute.OptionsProvider == null) + return inputAttribute.Options ?? (TryGetEnumOptions(propertyInfo, out var items) ? items : null); + + var providerType = inputAttribute.OptionsProvider; + + using var scope = _serviceProvider.CreateScope(); + var provider = (IActivityPropertyOptionsProvider) ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, providerType); + return provider.GetOptions(propertyInfo); + } + + private bool TryGetEnumOptions(PropertyInfo activityPropertyInfo, out IList? items) + { + var isNullable = activityPropertyInfo.PropertyType.IsNullableType(); + var propertyType = isNullable ? activityPropertyInfo.PropertyType.GetTypeOfNullable() : activityPropertyInfo.PropertyType; + + items = null; + + if (!propertyType.IsEnum) + return false; + + items = propertyType.GetEnumNames().Select(x => new SelectListItem(x.Humanize(LetterCasing.Title), x)).ToList(); + + if (isNullable) + items.Insert(0, new SelectListItem("-", "")); + + return true; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/TriggerDescriber.cs b/src/core/Elsa.Management/Services/TriggerDescriber.cs new file mode 100644 index 000000000..b7c1d8e73 --- /dev/null +++ b/src/core/Elsa.Management/Services/TriggerDescriber.cs @@ -0,0 +1,83 @@ +using System.ComponentModel; +using System.Reflection; +using Elsa.Attributes; +using Elsa.Helpers; +using Elsa.Management.Contracts; +using Elsa.Management.Models; +using Elsa.Models; +using Humanizer; + +namespace Elsa.Management.Services; + +public class TriggerDescriber : ITriggerDescriber +{ + private readonly IPropertyOptionsResolver _optionsResolver; + private readonly IPropertyDefaultValueResolver _defaultValueResolver; + private readonly ITriggerFactory _activityFactory; + + public TriggerDescriber(IPropertyOptionsResolver optionsResolver, IPropertyDefaultValueResolver defaultValueResolver, ITriggerFactory activityFactory) + { + _optionsResolver = optionsResolver; + _defaultValueResolver = defaultValueResolver; + _activityFactory = activityFactory; + } + + public ValueTask DescribeTriggerAsync(Type triggerType, CancellationToken cancellationToken = default) + { + var ns = TypeNameHelper.GenerateNamespace(triggerType); + var typeName = triggerType.Name; + var fullTypeName = TypeNameHelper.GenerateTypeName(triggerType, ns); + var displayNameAttr = triggerType.GetCustomAttribute(); + var displayName = displayNameAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title); + var categoryAttr = triggerType.GetCustomAttribute(); + var category = categoryAttr?.Category ?? TypeNameHelper.GetCategoryFromNamespace(ns) ?? "Miscellaneous"; + var descriptionAttr = triggerType.GetCustomAttribute(); + var description = descriptionAttr?.Description; + var properties = triggerType.GetProperties(); + var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType)).ToList(); + + var descriptor = new TriggerDescriptor + { + Category = category, + Description = description, + NodeType = fullTypeName, + DisplayName = displayName, + InputProperties = DescribeInputProperties(inputProperties).ToList(), + Constructor = context => + { + var activity = _activityFactory.Create(triggerType, context); + activity.TriggerType = fullTypeName; + return activity; + } + }; + + return ValueTask.FromResult(descriptor); + } + + private IEnumerable DescribeInputProperties(IEnumerable properties) + { + foreach (var propertyInfo in properties) + { + var inputAttribute = propertyInfo.GetCustomAttribute(); + var descriptionAttribute = propertyInfo.GetCustomAttribute(); + var wrappedPropertyType = propertyInfo.PropertyType.GenericTypeArguments[0]; + + yield return new InputDescriptor + ( + inputAttribute?.Name ?? propertyInfo.Name, + wrappedPropertyType, + InputUIHints.GetUIHint(wrappedPropertyType, inputAttribute), + inputAttribute?.DisplayName ?? propertyInfo.Name.Humanize(LetterCasing.Title), + descriptionAttribute?.Description, + _optionsResolver.GetOptions(propertyInfo), + inputAttribute?.Category, + inputAttribute?.Order ?? 0, + _defaultValueResolver.GetDefaultValue(propertyInfo), + inputAttribute?.DefaultSyntax, + inputAttribute?.SupportedSyntaxes, + inputAttribute?.IsReadOnly ?? false, + inputAttribute?.IsBrowsable ?? true + ); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/TriggerFactory.cs b/src/core/Elsa.Management/Services/TriggerFactory.cs new file mode 100644 index 000000000..d300c87cf --- /dev/null +++ b/src/core/Elsa.Management/Services/TriggerFactory.cs @@ -0,0 +1,15 @@ +using System.Text.Json; +using Elsa.Contracts; +using Elsa.Management.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Services; + +public class TriggerFactory : ITriggerFactory +{ + public ITrigger Create(Type type, TriggerConstructorContext context) + { + var trigger = (ITrigger)context.Element.Deserialize(type, context.SerializerOptions)!; + return trigger; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/TriggerRegistry.cs b/src/core/Elsa.Management/Services/TriggerRegistry.cs new file mode 100644 index 000000000..b8c5d96ee --- /dev/null +++ b/src/core/Elsa.Management/Services/TriggerRegistry.cs @@ -0,0 +1,58 @@ +using Elsa.Management.Contracts; +using Elsa.Management.Models; + +namespace Elsa.Management.Services; + +public class TriggerRegistry : ITriggerRegistry +{ + private readonly IDictionary> _providedTriggerDescriptors = new Dictionary>(); + private readonly IDictionary _triggerDescriptors = new Dictionary(); + + public void Add(object provider, TriggerDescriptor descriptor) => Add(descriptor, GetOrCreateDescriptors(provider)); + + public void AddMany(object provider, IEnumerable descriptors) + { + var target = GetOrCreateDescriptors(provider); + + foreach (var descriptor in descriptors) + Add(descriptor, target); + } + + public void Clear() + { + _triggerDescriptors.Clear(); + _providedTriggerDescriptors.Clear(); + } + + public void ClearProvider(object provider) + { + var descriptors = ListByProvider(provider).ToList(); + + foreach (var descriptor in descriptors) + _triggerDescriptors.Remove(descriptor.NodeType); + + _providedTriggerDescriptors.Remove(provider); + } + + public IEnumerable ListAll() => _triggerDescriptors.Values; + public IEnumerable ListByProvider(object provider) => _providedTriggerDescriptors.TryGetValue(provider, out var descriptors) ? descriptors : ArraySegment.Empty; + public TriggerDescriptor? Find(Func predicate) => _triggerDescriptors.Values.FirstOrDefault(predicate); + public TriggerDescriptor? Find(string triggerType) => _triggerDescriptors.TryGetValue(triggerType, out var descriptor) ? descriptor : null; + + private void Add(TriggerDescriptor descriptor, ICollection target) + { + _triggerDescriptors.Add(descriptor.NodeType, descriptor); + target.Add(descriptor); + } + + private ICollection GetOrCreateDescriptors(object provider) + { + if (_providedTriggerDescriptors.TryGetValue(provider, out var descriptors)) + return descriptors; + + descriptors = new List(); + _providedTriggerDescriptors.Add(provider, descriptors); + + return descriptors; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/TriggerRegistryPopulator.cs b/src/core/Elsa.Management/Services/TriggerRegistryPopulator.cs new file mode 100644 index 000000000..776e95c7a --- /dev/null +++ b/src/core/Elsa.Management/Services/TriggerRegistryPopulator.cs @@ -0,0 +1,27 @@ +using Elsa.Management.Contracts; + +namespace Elsa.Management.Services; + +/// +/// Populates the with descriptors provided by s. +/// +public class TriggerRegistryPopulator : ITriggerRegistryPopulator +{ + private readonly IEnumerable _providers; + private readonly ITriggerRegistry _registry; + + public TriggerRegistryPopulator(IEnumerable providers, ITriggerRegistry registry) + { + _providers = providers; + _registry = registry; + } + + public async ValueTask PopulateRegistryAsync(CancellationToken cancellationToken) + { + foreach (var provider in _providers) + { + var descriptors = await provider.GetDescriptorsAsync(cancellationToken); + _registry.AddMany(provider, descriptors); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/WorkflowPublisher.cs b/src/core/Elsa.Management/Services/WorkflowPublisher.cs new file mode 100644 index 000000000..fdc7e0740 --- /dev/null +++ b/src/core/Elsa.Management/Services/WorkflowPublisher.cs @@ -0,0 +1,169 @@ +using Elsa.Activities.Workflows; +using Elsa.Contracts; +using Elsa.Management.Contracts; +using Elsa.Management.Notifications; +using Elsa.Mediator.Contracts; +using Elsa.Models; +using Elsa.Persistence.Commands; +using Elsa.Persistence.Models; +using Elsa.Persistence.Requests; + +namespace Elsa.Management.Services +{ + public class WorkflowPublisher : IWorkflowPublisher + { + private readonly IMediator _mediator; + private readonly IIdentityGenerator _identityGenerator; + private readonly ISystemClock _systemClock; + + public WorkflowPublisher(IMediator mediator, IIdentityGenerator identityGenerator, ISystemClock systemClock) + { + _mediator = mediator; + _identityGenerator = identityGenerator; + _systemClock = systemClock; + } + + public Workflow New() + { + var id = _identityGenerator.GenerateId(); + var definitionId = _identityGenerator.GenerateId(); + const int version = 1; + + return new Workflow( + new WorkflowIdentity(definitionId, version, id), + WorkflowPublication.LatestDraft, + new WorkflowMetadata(CreatedAt: _systemClock.UtcNow), + new Sequence(), + new List()); + } + + public async Task PublishAsync(string definitionId, CancellationToken cancellationToken = default) + { + var workflow = await _mediator.RequestAsync(new FindWorkflow(definitionId, VersionOptions.Latest), cancellationToken); + + if (workflow == null) + return null; + + return await PublishAsync(workflow, cancellationToken); + } + + public async Task PublishAsync(Workflow workflow, CancellationToken cancellationToken = default) + { + var identity = workflow.Identity; + var definitionId = identity.DefinitionId; + + // Reset current latest and published definitions. + var publishedAndOrLatestWorkflows = await _mediator.RequestAsync(new FindLatestAndPublishedWorkflows(definitionId), cancellationToken); + + foreach (var publishedAndOrLatestWorkflow in publishedAndOrLatestWorkflows) + { + var updatedDraft = publishedAndOrLatestWorkflow with + { + Publication = WorkflowPublication.Draft + }; + + await _mediator.ExecuteAsync(new SaveWorkflow(updatedDraft), cancellationToken); + } + + workflow = workflow.Publication.IsPublished ? workflow.IncrementVersion() : workflow.WithPublished(); + + workflow.WithLatest(); + workflow = Initialize(workflow); + + await _mediator.PublishAsync(new WorkflowPublishing(workflow), cancellationToken); + await _mediator.ExecuteAsync(new SaveWorkflow(workflow), cancellationToken); + await _mediator.PublishAsync(new WorkflowPublished(workflow), cancellationToken); + return workflow; + } + + public async Task RetractAsync(string definitionId, CancellationToken cancellationToken = default) + { + var workflow = await _mediator.RequestAsync(new FindWorkflow(definitionId, VersionOptions.Published), cancellationToken); + + if (workflow == null) + return null; + + return await RetractAsync(workflow, cancellationToken); + } + + public async Task RetractAsync(Workflow workflow, CancellationToken cancellationToken = default) + { + if (!workflow.Publication.IsPublished) + throw new InvalidOperationException("Cannot unpublish an unpublished workflow definition."); + + workflow.WithPublished(false); + workflow = Initialize(workflow); + + await _mediator.PublishAsync(new WorkflowRetracting(workflow), cancellationToken); + await _mediator.ExecuteAsync(new SaveWorkflow(workflow), cancellationToken); + await _mediator.PublishAsync(new WorkflowRetracted(workflow), cancellationToken); + return workflow; + } + + public async Task GetDraftAsync(string definitionId, CancellationToken cancellationToken = default) + { + var workflow = await _mediator.RequestAsync(new FindWorkflow(definitionId, VersionOptions.Latest), cancellationToken); + + if (workflow == null) + return null; + + if (!workflow.Publication.IsPublished) + return workflow; + + var draft = workflow with + { + Identity = new WorkflowIdentity( + workflow.Identity.DefinitionId, + workflow.Identity.Version + 1, + _identityGenerator.GenerateId()), + Publication = WorkflowPublication.LatestDraft + }; + + return draft; + } + + public async Task SaveDraftAsync(Workflow workflow, CancellationToken cancellationToken = default) + { + var draft = workflow; + var definitionId = workflow.Identity.DefinitionId; + var latestVersion = await _mediator.RequestAsync(new FindWorkflow(definitionId, VersionOptions.Latest), cancellationToken); + + if (latestVersion?.Publication is { IsPublished: true, IsLatest: true }) + { + latestVersion = latestVersion.WithLatest(false); + await _mediator.ExecuteAsync(new SaveWorkflow(latestVersion), cancellationToken); + } + + draft = draft with + { + Publication = WorkflowPublication.LatestDraft + }; + draft = Initialize(draft); + + await _mediator.ExecuteAsync(new SaveWorkflow(draft), cancellationToken); + return draft; + } + + public async Task DeleteAsync(string definitionId, CancellationToken cancellationToken = default) + { + await _mediator.ExecuteAsync(new DeleteWorkflowInstances(definitionId), cancellationToken); + await _mediator.ExecuteAsync(new DeleteWorkflowDefinition(definitionId), cancellationToken); + } + + public Task DeleteAsync(Workflow workflow, CancellationToken cancellationToken = default) => DeleteAsync(workflow.Identity.DefinitionId, cancellationToken); + + private Workflow Initialize(Workflow workflow) + { + if (workflow.Identity.Id == null!) + workflow = workflow.WithId(_identityGenerator.GenerateId()); + + if (workflow.Identity.DefinitionId == null!) + workflow = workflow.WithDefinitionId(_identityGenerator.GenerateId()); + + if (workflow.Identity.Version == 0) + workflow.WithVersion(1); + + return workflow; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/ICommand.cs b/src/core/Elsa.Mediator/Contracts/ICommand.cs new file mode 100644 index 000000000..9262855d9 --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/ICommand.cs @@ -0,0 +1,9 @@ +namespace Elsa.Mediator.Contracts; + +public interface ICommand +{ +} + +public interface ICommand : ICommand +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/ICommandHandler.cs b/src/core/Elsa.Mediator/Contracts/ICommandHandler.cs new file mode 100644 index 000000000..7b3ae0c0f --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/ICommandHandler.cs @@ -0,0 +1,14 @@ +namespace Elsa.Mediator.Contracts; + +public interface ICommandHandler +{ +} + +public interface ICommandHandler : ICommandHandler where TCommand : ICommand +{ + Task HandleAsync(TCommand command, CancellationToken cancellationToken); +} + +public interface ICommandHandler : ICommandHandler where TCommand : ICommand +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/ICommandSender.cs b/src/core/Elsa.Mediator/Contracts/ICommandSender.cs new file mode 100644 index 000000000..e21499b56 --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/ICommandSender.cs @@ -0,0 +1,7 @@ +namespace Elsa.Mediator.Contracts; + +public interface ICommandSender +{ + Task ExecuteAsync(ICommand command, CancellationToken cancellationToken = default); + Task ExecuteAsync(ICommand command, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/IMediator.cs b/src/core/Elsa.Mediator/Contracts/IMediator.cs new file mode 100644 index 000000000..be568f1ba --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/IMediator.cs @@ -0,0 +1,5 @@ +namespace Elsa.Mediator.Contracts; + +public interface IMediator : IRequestSender, ICommandSender, IPublisher +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/INotification.cs b/src/core/Elsa.Mediator/Contracts/INotification.cs new file mode 100644 index 000000000..77ec73f0f --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/INotification.cs @@ -0,0 +1,6 @@ +namespace Elsa.Mediator.Contracts; + +public interface INotification +{ + +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/INotificationHandler.cs b/src/core/Elsa.Mediator/Contracts/INotificationHandler.cs new file mode 100644 index 000000000..6d3e1184c --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/INotificationHandler.cs @@ -0,0 +1,10 @@ +namespace Elsa.Mediator.Contracts; + +public interface INotificationHandler +{ +} + +public interface INotificationHandler : INotificationHandler where T : INotification +{ + Task HandleAsync(T notification, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/IPublisher.cs b/src/core/Elsa.Mediator/Contracts/IPublisher.cs new file mode 100644 index 000000000..7325370cb --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/IPublisher.cs @@ -0,0 +1,6 @@ +namespace Elsa.Mediator.Contracts; + +public interface IPublisher +{ + Task PublishAsync(INotification notification, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/IRequest.cs b/src/core/Elsa.Mediator/Contracts/IRequest.cs new file mode 100644 index 000000000..0ccc7e6a4 --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/IRequest.cs @@ -0,0 +1,9 @@ +namespace Elsa.Mediator.Contracts; + +public interface IRequest +{ +} + +public interface IRequest : IRequest +{ +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/IRequestHandler.cs b/src/core/Elsa.Mediator/Contracts/IRequestHandler.cs new file mode 100644 index 000000000..4ffaeaa94 --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/IRequestHandler.cs @@ -0,0 +1,10 @@ +namespace Elsa.Mediator.Contracts; + +public interface IRequestHandler +{ +} + +public interface IRequestHandler : IRequestHandler where TRequest : IRequest? +{ + Task HandleAsync(TRequest request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/IRequestSender.cs b/src/core/Elsa.Mediator/Contracts/IRequestSender.cs new file mode 100644 index 000000000..2f5bdcab6 --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/IRequestSender.cs @@ -0,0 +1,6 @@ +namespace Elsa.Mediator.Contracts; + +public interface IRequestSender +{ + Task RequestAsync(IRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Contracts/Unit.cs b/src/core/Elsa.Mediator/Contracts/Unit.cs new file mode 100644 index 000000000..bd1711c59 --- /dev/null +++ b/src/core/Elsa.Mediator/Contracts/Unit.cs @@ -0,0 +1,6 @@ +namespace Elsa.Mediator.Contracts; + +public record Unit +{ + public static readonly Unit Instance = new (); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Elsa.Mediator.csproj b/src/core/Elsa.Mediator/Elsa.Mediator.csproj new file mode 100644 index 000000000..168c16fd2 --- /dev/null +++ b/src/core/Elsa.Mediator/Elsa.Mediator.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/core/Elsa.Mediator/Extensions/ServiceCollectionExtensions.cs b/src/core/Elsa.Mediator/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..0798be284 --- /dev/null +++ b/src/core/Elsa.Mediator/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,84 @@ +using System.Reflection; +using Elsa.Mediator.Contracts; +using Elsa.Mediator.Middleware.Command; +using Elsa.Mediator.Middleware.Command.Contracts; +using Elsa.Mediator.Middleware.Notification; +using Elsa.Mediator.Middleware.Notification.Contracts; +using Elsa.Mediator.Middleware.Request; +using Elsa.Mediator.Middleware.Request.Contracts; +using Elsa.Mediator.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Mediator.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMediator(this IServiceCollection services) + { + return services + .AddSingleton() + .AddSingleton(sp => sp.GetRequiredService()) + .AddSingleton(sp => sp.GetRequiredService()) + .AddSingleton(sp => sp.GetRequiredService()) + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } + + public static IServiceCollection AddCommandHandler(this IServiceCollection services) + where THandler : class, ICommandHandler + where TCommand : ICommand => + services.AddCommandHandler(); + + public static IServiceCollection AddCommandHandler(this IServiceCollection services) + where THandler : class, ICommandHandler + where TCommand : ICommand + { + return services.AddSingleton(); + } + + public static IServiceCollection AddRequestHandler(this IServiceCollection services) + where THandler : class, IRequestHandler + where TRequest : IRequest? + { + return services.AddSingleton(); + } + + public static IServiceCollection AddHandlersFrom(this IServiceCollection services) => services.AddHandlersFrom(typeof(TMarker)); + public static IServiceCollection AddHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFrom(markerType.Assembly); + + public static IServiceCollection AddHandlersFrom(this IServiceCollection services, Assembly assembly) + { + return services + .AddNotificationHandlersFrom(assembly) + .AddRequestHandlersFrom(assembly) + .AddCommandHandlersFrom(assembly); + } + + public static IServiceCollection AddNotificationHandlersFrom(this IServiceCollection services) => services.AddHandlersFromInternal(typeof(TMarker)); + public static IServiceCollection AddNotificationHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFromInternal(markerType); + public static IServiceCollection AddNotificationHandlersFrom(this IServiceCollection services, Assembly assembly) => services.AddHandlersFromInternal(assembly); + + public static IServiceCollection AddRequestHandlersFrom(this IServiceCollection services) => services.AddHandlersFromInternal(typeof(TMarker)); + public static IServiceCollection AddRequestHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFromInternal(markerType); + public static IServiceCollection AddRequestHandlersFrom(this IServiceCollection services, Assembly assembly) => services.AddHandlersFromInternal(assembly); + + public static IServiceCollection AddCommandHandlersFrom(this IServiceCollection services) => services.AddHandlersFromInternal(typeof(TMarker)); + public static IServiceCollection AddCommandHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFromInternal(markerType); + public static IServiceCollection AddCommandHandlersFrom(this IServiceCollection services, Assembly assembly) => services.AddHandlersFromInternal(assembly); + + + private static IServiceCollection AddHandlersFromInternal(this IServiceCollection services) => services.AddHandlersFromInternal(typeof(TMarker)); + private static IServiceCollection AddHandlersFromInternal(this IServiceCollection services, Type assemblyMarkerType) => services.AddHandlersFromInternal(assemblyMarkerType.Assembly); + + private static IServiceCollection AddHandlersFromInternal(this IServiceCollection services, Assembly assembly) + { + var serviceType = typeof(TService); + var types = assembly.ExportedTypes.Where(x => serviceType.IsAssignableFrom(x)); + + foreach (var type in types) + services.AddSingleton(serviceType, type); + + return services; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/CommandContext.cs b/src/core/Elsa.Mediator/Middleware/Command/CommandContext.cs new file mode 100644 index 000000000..d43ba8edf --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/CommandContext.cs @@ -0,0 +1,18 @@ +using Elsa.Mediator.Contracts; + +namespace Elsa.Mediator.Middleware.Command; + +public class CommandContext +{ + public CommandContext(ICommand command, Type resultType, CancellationToken cancellationToken) + { + Command = command; + ResultType = resultType; + CancellationToken = cancellationToken; + } + + public ICommand Command { get; init; } + public Type ResultType { get; init; } + public CancellationToken CancellationToken { get; init; } + public object? Result { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/CommandMiddlewareDelegate.cs b/src/core/Elsa.Mediator/Middleware/Command/CommandMiddlewareDelegate.cs new file mode 100644 index 000000000..77745fed3 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/CommandMiddlewareDelegate.cs @@ -0,0 +1,3 @@ +namespace Elsa.Mediator.Middleware.Command; + +public delegate ValueTask CommandMiddlewareDelegate(CommandContext context); \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/CommandPipeline.cs b/src/core/Elsa.Mediator/Middleware/Command/CommandPipeline.cs new file mode 100644 index 000000000..cf1423786 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/CommandPipeline.cs @@ -0,0 +1,24 @@ +using Elsa.Mediator.Middleware.Command.Contracts; + +namespace Elsa.Mediator.Middleware.Command; + +public class CommandPipeline : ICommandPipeline +{ + private readonly IServiceProvider _serviceProvider; + private CommandMiddlewareDelegate? _pipeline; + + public CommandPipeline(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public CommandMiddlewareDelegate Pipeline => _pipeline ??= CreateDefaultPipeline(); + + public CommandMiddlewareDelegate Setup(Action? setup = default) + { + var builder = new CommandPipelineBuilder(_serviceProvider); + setup?.Invoke(builder); + _pipeline = builder.Build(); + return _pipeline; + } + + public async Task ExecuteAsync(CommandContext context) => await Pipeline(context); + + private CommandMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x.UseCommandHandlers()); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/CommandPipelineBuilder.cs b/src/core/Elsa.Mediator/Middleware/Command/CommandPipelineBuilder.cs new file mode 100644 index 000000000..d29c5ede3 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/CommandPipelineBuilder.cs @@ -0,0 +1,41 @@ +using Elsa.Mediator.Middleware.Command.Contracts; + +namespace Elsa.Mediator.Middleware.Command; + +public class CommandPipelineBuilder : ICommandPipelineBuilder +{ + private const string ServicesKey = "mediator.Services"; + private readonly IList> _components = new List>(); + + public CommandPipelineBuilder(IServiceProvider serviceProvider) + { + ApplicationServices = serviceProvider; + } + + public IDictionary Properties { get; } = new Dictionary(); + + public IServiceProvider ApplicationServices + { + get => GetProperty(ServicesKey)!; + set => SetProperty(ServicesKey, value); + } + + public ICommandPipelineBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + public CommandMiddlewareDelegate Build() + { + CommandMiddlewareDelegate pipeline = _ => new ValueTask(); + + foreach (var component in _components.Reverse()) + pipeline = component(pipeline); + + return pipeline; + } + + private T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default(T); + private void SetProperty(string key, T value) => Properties[key] = value; +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/CommandPipelineBuilderExtensions.cs b/src/core/Elsa.Mediator/Middleware/Command/CommandPipelineBuilderExtensions.cs new file mode 100644 index 000000000..23908a18b --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/CommandPipelineBuilderExtensions.cs @@ -0,0 +1,10 @@ +using Elsa.Mediator.Middleware.Command.Contracts; +using Elsa.Mediator.Middleware.Command.Components; + +namespace Elsa.Mediator.Middleware.Command; + +public static class CommandPipelineBuilderExtensions +{ + public static ICommandPipelineBuilder UseCommandHandlers(this ICommandPipelineBuilder builder) => builder.UseMiddleware(); + public static ICommandPipelineBuilder UseCommandLogging(this ICommandPipelineBuilder builder) => builder.UseMiddleware(); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/Components/CommandHandlerInvokerMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Command/Components/CommandHandlerInvokerMiddleware.cs new file mode 100644 index 000000000..540ef6761 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/Components/CommandHandlerInvokerMiddleware.cs @@ -0,0 +1,46 @@ +using Elsa.Mediator.Contracts; +using Elsa.Mediator.Middleware.Command.Contracts; + +namespace Elsa.Mediator.Middleware.Command.Components; + +public class CommandHandlerInvokerMiddleware : ICommandMiddleware +{ + private readonly CommandMiddlewareDelegate _next; + private readonly IEnumerable _commandHandlers; + + public CommandHandlerInvokerMiddleware(CommandMiddlewareDelegate next, IEnumerable commandHandlers) + { + _next = next; + _commandHandlers = commandHandlers; + } + + public async ValueTask InvokeAsync(CommandContext context) + { + // Find all handlers for the specified command. + var command = context.Command; + var commandType = command.GetType(); + var resultType = context.ResultType; + var handlerType = typeof(ICommandHandler<,>).MakeGenericType(commandType, resultType); + var handlers = _commandHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray(); + + if (!handlers.Any()) + throw new InvalidOperationException($"There is no handler to handle the {commandType.FullName} command"); + + if (handlers.Length > 1) + throw new InvalidOperationException($"Multiple handlers were found to handle the {commandType.FullName} command"); + + var handler = handlers.First(); + var handleMethod = handlerType.GetMethod("HandleAsync")!; + var cancellationToken = context.CancellationToken; + var task = (Task)handleMethod.Invoke(handler, new object?[] { command, cancellationToken })!; + await task; + + // Get result of task. + var taskWithReturnType = typeof(Task<>).MakeGenericType(resultType); + var resultProperty = taskWithReturnType.GetProperty(nameof(Task.Result))!; + context.Result = resultProperty.GetValue(task); + + // Invoke next middleware. + await _next(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/Components/CommandLoggingMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Command/Components/CommandLoggingMiddleware.cs new file mode 100644 index 000000000..7158403cf --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/Components/CommandLoggingMiddleware.cs @@ -0,0 +1,15 @@ +using Elsa.Mediator.Middleware.Command.Contracts; + +namespace Elsa.Mediator.Middleware.Command.Components; + +public class CommandLoggingMiddleware : ICommandMiddleware +{ + private readonly CommandMiddlewareDelegate _next; + public CommandLoggingMiddleware(CommandMiddlewareDelegate next) => _next = next; + + public async ValueTask InvokeAsync(CommandContext context) + { + // Invoke next middleware. + await _next(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandMiddleware.cs new file mode 100644 index 000000000..9fbaac397 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandMiddleware.cs @@ -0,0 +1,6 @@ +namespace Elsa.Mediator.Middleware.Command.Contracts; + +public interface ICommandMiddleware +{ + ValueTask InvokeAsync(CommandContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandPipeline.cs b/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandPipeline.cs new file mode 100644 index 000000000..c8936065e --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandPipeline.cs @@ -0,0 +1,8 @@ +namespace Elsa.Mediator.Middleware.Command.Contracts; + +public interface ICommandPipeline +{ + CommandMiddlewareDelegate Setup(Action setup); + CommandMiddlewareDelegate Pipeline { get; } + Task ExecuteAsync(CommandContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandPipelineBuilder.cs b/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandPipelineBuilder.cs new file mode 100644 index 000000000..ffe60193f --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/Contracts/ICommandPipelineBuilder.cs @@ -0,0 +1,9 @@ +namespace Elsa.Mediator.Middleware.Command.Contracts; + +public interface ICommandPipelineBuilder +{ + public IDictionary Properties { get; } + IServiceProvider ApplicationServices { get; } + ICommandPipelineBuilder Use(Func middleware); + public CommandMiddlewareDelegate Build(); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Command/MiddlewareExtensions.cs b/src/core/Elsa.Mediator/Middleware/Command/MiddlewareExtensions.cs new file mode 100644 index 000000000..71ab1f2b8 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Command/MiddlewareExtensions.cs @@ -0,0 +1,20 @@ +using Elsa.Mediator.Middleware.Command.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Mediator.Middleware.Command; + +public static class MiddlewareExtensions +{ + public static ICommandPipelineBuilder UseMiddleware(this ICommandPipelineBuilder builder, params object[] args) where TMiddleware: ICommandMiddleware + { + var middleware = typeof(TMiddleware); + + return builder.Use(next => + { + var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware); + var ctorParams = new[] { next }.Concat(args).Select(x => x!).ToArray(); + var instance = ActivatorUtilities.CreateInstance(builder.ApplicationServices, middleware, ctorParams); + return (CommandMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(CommandMiddlewareDelegate), instance); + }); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/MiddlewareHelpers.cs b/src/core/Elsa.Mediator/Middleware/MiddlewareHelpers.cs new file mode 100644 index 000000000..621a7a5a3 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/MiddlewareHelpers.cs @@ -0,0 +1,29 @@ +using System.Reflection; + +namespace Elsa.Mediator.Middleware; + +public static class MiddlewareHelpers +{ + public static MethodInfo GetInvokeMethod(Type middleware) + { + const string invokeMethodName = "Invoke"; + const string invokeAsyncMethodName = "InvokeAsync"; + var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); + var invokeMethods = methods.Where(m => string.Equals(m.Name, invokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, invokeAsyncMethodName, StringComparison.Ordinal)).ToArray(); + + switch (invokeMethods.Length) + { + case > 1: + throw new InvalidOperationException("Multiple Invoke methods were found. Use either Invoke or InvokeAsync."); + case 0: + throw new InvalidOperationException("No Invoke methods were found. Use either Invoke or InvokeAsync"); + } + + var methodInfo = invokeMethods[0]; + + if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType) && !typeof(ValueTask).IsAssignableFrom(methodInfo.ReturnType)) + throw new InvalidOperationException($"The {methodInfo.Name} method must return Task or ValueTask"); + + return methodInfo; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/Components/NotificationHandlerInvokerMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Notification/Components/NotificationHandlerInvokerMiddleware.cs new file mode 100644 index 000000000..fc7a8a0a1 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/Components/NotificationHandlerInvokerMiddleware.cs @@ -0,0 +1,36 @@ +using Elsa.Mediator.Contracts; +using Elsa.Mediator.Middleware.Notification.Contracts; + +namespace Elsa.Mediator.Middleware.Notification.Components; + +public class NotificationHandlerInvokerMiddleware : INotificationMiddleware +{ + private readonly NotificationMiddlewareDelegate _next; + private readonly IEnumerable _notificationHandlers; + + public NotificationHandlerInvokerMiddleware(NotificationMiddlewareDelegate next, IEnumerable notificationHandlers) + { + _next = next; + _notificationHandlers = notificationHandlers; + } + + public async ValueTask InvokeAsync(NotificationContext context) + { + // Find all handlers for the specified notification. + var notification = context.Notification; + var notificationType = notification.GetType(); + var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); + var handlers = _notificationHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray(); + var handleMethod = handlerType.GetMethod("HandleAsync")!; + var cancellationToken = context.CancellationToken; + + foreach (var handler in handlers) + { + var task = (Task)handleMethod.Invoke(handler, new object?[] { notification, cancellationToken })!; + await task; + } + + // Invoke next middleware. + await _next(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/Components/NotificationLoggingMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Notification/Components/NotificationLoggingMiddleware.cs new file mode 100644 index 000000000..d035b1f89 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/Components/NotificationLoggingMiddleware.cs @@ -0,0 +1,15 @@ +using Elsa.Mediator.Middleware.Notification.Contracts; + +namespace Elsa.Mediator.Middleware.Notification.Components; + +public class NotificationLoggingMiddleware : INotificationMiddleware +{ + private readonly NotificationMiddlewareDelegate _next; + public NotificationLoggingMiddleware(NotificationMiddlewareDelegate next) => _next = next; + + public async ValueTask InvokeAsync(NotificationContext context) + { + // Invoke next middleware. + await _next(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationMiddleware.cs new file mode 100644 index 000000000..31da7451f --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationMiddleware.cs @@ -0,0 +1,6 @@ +namespace Elsa.Mediator.Middleware.Notification.Contracts; + +public interface INotificationMiddleware +{ + ValueTask InvokeAsync(NotificationContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationPipeline.cs b/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationPipeline.cs new file mode 100644 index 000000000..630b30996 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationPipeline.cs @@ -0,0 +1,8 @@ +namespace Elsa.Mediator.Middleware.Notification.Contracts; + +public interface INotificationPipeline +{ + NotificationMiddlewareDelegate Setup(Action setup); + NotificationMiddlewareDelegate Pipeline { get; } + Task ExecuteAsync(NotificationContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationPipelineBuilder.cs b/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationPipelineBuilder.cs new file mode 100644 index 000000000..64971aacf --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/Contracts/INotificationPipelineBuilder.cs @@ -0,0 +1,9 @@ +namespace Elsa.Mediator.Middleware.Notification.Contracts; + +public interface INotificationPipelineBuilder +{ + public IDictionary Properties { get; } + IServiceProvider ApplicationServices { get; } + INotificationPipelineBuilder Use(Func middleware); + public NotificationMiddlewareDelegate Build(); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/MiddlewareExtensions.cs b/src/core/Elsa.Mediator/Middleware/Notification/MiddlewareExtensions.cs new file mode 100644 index 000000000..497904844 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/MiddlewareExtensions.cs @@ -0,0 +1,20 @@ +using Elsa.Mediator.Middleware.Notification.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Mediator.Middleware.Notification; + +public static class MiddlewareExtensions +{ + public static INotificationPipelineBuilder UseMiddleware(this INotificationPipelineBuilder builder, params object[] args) where TMiddleware: INotificationMiddleware + { + var middleware = typeof(TMiddleware); + + return builder.Use(next => + { + var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware); + var ctorParams = new[] { next }.Concat(args).Select(x => x!).ToArray(); + var instance = ActivatorUtilities.CreateInstance(builder.ApplicationServices, middleware, ctorParams); + return (NotificationMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(NotificationMiddlewareDelegate), instance); + }); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/NotificationContext.cs b/src/core/Elsa.Mediator/Middleware/Notification/NotificationContext.cs new file mode 100644 index 000000000..229115fc6 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/NotificationContext.cs @@ -0,0 +1,15 @@ +using Elsa.Mediator.Contracts; + +namespace Elsa.Mediator.Middleware.Notification; + +public class NotificationContext +{ + public NotificationContext(INotification notification, CancellationToken cancellationToken) + { + Notification = notification; + CancellationToken = cancellationToken; + } + + public INotification Notification { get; init; } + public CancellationToken CancellationToken { get; init; } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/NotificationMiddlewareDelegate.cs b/src/core/Elsa.Mediator/Middleware/Notification/NotificationMiddlewareDelegate.cs new file mode 100644 index 000000000..f9503c212 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/NotificationMiddlewareDelegate.cs @@ -0,0 +1,3 @@ +namespace Elsa.Mediator.Middleware.Notification; + +public delegate ValueTask NotificationMiddlewareDelegate(NotificationContext context); \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipeline.cs b/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipeline.cs new file mode 100644 index 000000000..483ef73bc --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipeline.cs @@ -0,0 +1,24 @@ +using Elsa.Mediator.Middleware.Notification.Contracts; + +namespace Elsa.Mediator.Middleware.Notification; + +public class NotificationPipeline : INotificationPipeline +{ + private readonly IServiceProvider _serviceProvider; + private NotificationMiddlewareDelegate? _pipeline; + + public NotificationPipeline(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public NotificationMiddlewareDelegate Pipeline => _pipeline ??= CreateDefaultPipeline(); + + public NotificationMiddlewareDelegate Setup(Action? setup = default) + { + var builder = new NotificationPipelineBuilder(_serviceProvider); + setup?.Invoke(builder); + _pipeline = builder.Build(); + return _pipeline; + } + + public async Task ExecuteAsync(NotificationContext context) => await Pipeline(context); + + private NotificationMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x.UseNotificationHandlers()); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipelineBuilder.cs b/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipelineBuilder.cs new file mode 100644 index 000000000..3fdaf810e --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipelineBuilder.cs @@ -0,0 +1,41 @@ +using Elsa.Mediator.Middleware.Notification.Contracts; + +namespace Elsa.Mediator.Middleware.Notification; + +public class NotificationPipelineBuilder : INotificationPipelineBuilder +{ + private const string ServicesKey = "mediator.Services"; + private readonly IList> _components = new List>(); + + public NotificationPipelineBuilder(IServiceProvider serviceProvider) + { + ApplicationServices = serviceProvider; + } + + public IDictionary Properties { get; } = new Dictionary(); + + public IServiceProvider ApplicationServices + { + get => GetProperty(ServicesKey)!; + set => SetProperty(ServicesKey, value); + } + + public INotificationPipelineBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + public NotificationMiddlewareDelegate Build() + { + NotificationMiddlewareDelegate pipeline = _ => new ValueTask(); + + foreach (var component in _components.Reverse()) + pipeline = component(pipeline); + + return pipeline; + } + + private T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default(T); + private void SetProperty(string key, T value) => Properties[key] = value; +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipelineBuilderExtensions.cs b/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipelineBuilderExtensions.cs new file mode 100644 index 000000000..8d4fa5bc6 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Notification/NotificationPipelineBuilderExtensions.cs @@ -0,0 +1,10 @@ +using Elsa.Mediator.Middleware.Notification.Components; +using Elsa.Mediator.Middleware.Notification.Contracts; + +namespace Elsa.Mediator.Middleware.Notification; + +public static class NotificationPipelineBuilderExtensions +{ + public static INotificationPipelineBuilder UseNotificationHandlers(this INotificationPipelineBuilder builder) => builder.UseMiddleware(); + public static INotificationPipelineBuilder UseNotificationLogging(this INotificationPipelineBuilder builder) => builder.UseMiddleware(); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/Components/RequestHandlerInvokerMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Request/Components/RequestHandlerInvokerMiddleware.cs new file mode 100644 index 000000000..ac3bd8dd1 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/Components/RequestHandlerInvokerMiddleware.cs @@ -0,0 +1,46 @@ +using Elsa.Mediator.Contracts; +using Elsa.Mediator.Middleware.Request.Contracts; + +namespace Elsa.Mediator.Middleware.Request.Components; + +public class RequestHandlerInvokerMiddleware : IRequestMiddleware +{ + private readonly RequestMiddlewareDelegate _next; + private readonly IEnumerable _requestHandlers; + + public RequestHandlerInvokerMiddleware(RequestMiddlewareDelegate next, IEnumerable requestHandlers) + { + _next = next; + _requestHandlers = requestHandlers; + } + + public async ValueTask InvokeAsync(RequestContext context) + { + // Find all handlers for the specified request. + var request = context.Request; + var requestType = request.GetType(); + var responseType = context.ResponseType; + var handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, responseType); + var handlers = _requestHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray(); + + if (!handlers.Any()) + throw new InvalidOperationException($"There is no handler to handle the {requestType.FullName} request"); + + if (handlers.Length > 1) + throw new InvalidOperationException($"Multiple handlers were found to handle the {requestType.FullName} request"); + + var handler = handlers.First(); + var handleMethod = handlerType.GetMethod("HandleAsync")!; + var cancellationToken = context.CancellationToken; + var task = (Task)handleMethod.Invoke(handler, new object?[] { request, cancellationToken })!; + await task; + + // Get result of task. + var taskWithReturnType = typeof(Task<>).MakeGenericType(responseType); + var resultProperty = taskWithReturnType.GetProperty(nameof(Task.Result))!; + context.Response = resultProperty.GetValue(task); + + // Invoke next middleware. + await _next(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/Components/RequestLoggingMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Request/Components/RequestLoggingMiddleware.cs new file mode 100644 index 000000000..174c1980b --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/Components/RequestLoggingMiddleware.cs @@ -0,0 +1,15 @@ +using Elsa.Mediator.Middleware.Request.Contracts; + +namespace Elsa.Mediator.Middleware.Request.Components; + +public class RequestLoggingMiddleware : IRequestMiddleware +{ + private readonly RequestMiddlewareDelegate _next; + public RequestLoggingMiddleware(RequestMiddlewareDelegate next) => _next = next; + + public async ValueTask InvokeAsync(RequestContext context) + { + // Invoke next middleware. + await _next(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestMiddleware.cs b/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestMiddleware.cs new file mode 100644 index 000000000..e3edfb7fe --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestMiddleware.cs @@ -0,0 +1,6 @@ +namespace Elsa.Mediator.Middleware.Request.Contracts; + +public interface IRequestMiddleware +{ + ValueTask InvokeAsync(RequestContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestPipeline.cs b/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestPipeline.cs new file mode 100644 index 000000000..d3cf252a3 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestPipeline.cs @@ -0,0 +1,8 @@ +namespace Elsa.Mediator.Middleware.Request.Contracts; + +public interface IRequestPipeline +{ + RequestMiddlewareDelegate Setup(Action setup); + RequestMiddlewareDelegate Pipeline { get; } + Task ExecuteAsync(RequestContext context); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestPipelineBuilder.cs b/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestPipelineBuilder.cs new file mode 100644 index 000000000..8f245195c --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/Contracts/IRequestPipelineBuilder.cs @@ -0,0 +1,9 @@ +namespace Elsa.Mediator.Middleware.Request.Contracts; + +public interface IRequestPipelineBuilder +{ + public IDictionary Properties { get; } + IServiceProvider ApplicationServices { get; } + IRequestPipelineBuilder Use(Func middleware); + public RequestMiddlewareDelegate Build(); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/MiddlewareExtensions.cs b/src/core/Elsa.Mediator/Middleware/Request/MiddlewareExtensions.cs new file mode 100644 index 000000000..9ecbd1039 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/MiddlewareExtensions.cs @@ -0,0 +1,20 @@ +using Elsa.Mediator.Middleware.Request.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Mediator.Middleware.Request; + +public static class MiddlewareExtensions +{ + public static IRequestPipelineBuilder UseMiddleware(this IRequestPipelineBuilder builder, params object[] args) where TMiddleware: IRequestMiddleware + { + var middleware = typeof(TMiddleware); + + return builder.Use(next => + { + var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware); + var ctorParams = new[] { next }.Concat(args).Select(x => x!).ToArray(); + var instance = ActivatorUtilities.CreateInstance(builder.ApplicationServices, middleware, ctorParams); + return (RequestMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(RequestMiddlewareDelegate), instance); + }); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/RequestContext.cs b/src/core/Elsa.Mediator/Middleware/Request/RequestContext.cs new file mode 100644 index 000000000..01ce217de --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/RequestContext.cs @@ -0,0 +1,18 @@ +using Elsa.Mediator.Contracts; + +namespace Elsa.Mediator.Middleware.Request; + +public class RequestContext +{ + public RequestContext(IRequest request, Type responseType, CancellationToken cancellationToken) + { + Request = request; + ResponseType = responseType; + CancellationToken = cancellationToken; + } + + public IRequest Request { get; init; } + public Type ResponseType { get; init; } + public CancellationToken CancellationToken { get; init; } + public object? Response { get; set; } +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/RequestMiddlewareDelegate.cs b/src/core/Elsa.Mediator/Middleware/Request/RequestMiddlewareDelegate.cs new file mode 100644 index 000000000..30f1c806a --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/RequestMiddlewareDelegate.cs @@ -0,0 +1,3 @@ +namespace Elsa.Mediator.Middleware.Request; + +public delegate ValueTask RequestMiddlewareDelegate(RequestContext context); \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/RequestPipeline.cs b/src/core/Elsa.Mediator/Middleware/Request/RequestPipeline.cs new file mode 100644 index 000000000..5facde19f --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/RequestPipeline.cs @@ -0,0 +1,24 @@ +using Elsa.Mediator.Middleware.Request.Contracts; + +namespace Elsa.Mediator.Middleware.Request; + +public class RequestPipeline : IRequestPipeline +{ + private readonly IServiceProvider _serviceProvider; + private RequestMiddlewareDelegate? _pipeline; + + public RequestPipeline(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public RequestMiddlewareDelegate Pipeline => _pipeline ??= CreateDefaultPipeline(); + + public RequestMiddlewareDelegate Setup(Action? setup = default) + { + var builder = new RequestPipelineBuilder(_serviceProvider); + setup?.Invoke(builder); + _pipeline = builder.Build(); + return _pipeline; + } + + public async Task ExecuteAsync(RequestContext context) => await Pipeline(context); + + private RequestMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x.UseRequestHandlers()); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/RequestPipelineBuilder.cs b/src/core/Elsa.Mediator/Middleware/Request/RequestPipelineBuilder.cs new file mode 100644 index 000000000..b4785da92 --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/RequestPipelineBuilder.cs @@ -0,0 +1,41 @@ +using Elsa.Mediator.Middleware.Request.Contracts; + +namespace Elsa.Mediator.Middleware.Request; + +public class RequestPipelineBuilder : IRequestPipelineBuilder +{ + private const string ServicesKey = "mediator.Services"; + private readonly IList> _components = new List>(); + + public RequestPipelineBuilder(IServiceProvider serviceProvider) + { + ApplicationServices = serviceProvider; + } + + public IDictionary Properties { get; } = new Dictionary(); + + public IServiceProvider ApplicationServices + { + get => GetProperty(ServicesKey)!; + set => SetProperty(ServicesKey, value); + } + + public IRequestPipelineBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + public RequestMiddlewareDelegate Build() + { + RequestMiddlewareDelegate pipeline = _ => new ValueTask(); + + foreach (var component in _components.Reverse()) + pipeline = component(pipeline); + + return pipeline; + } + + private T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default(T); + private void SetProperty(string key, T value) => Properties[key] = value; +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Middleware/Request/RequestPipelineBuilderExtensions.cs b/src/core/Elsa.Mediator/Middleware/Request/RequestPipelineBuilderExtensions.cs new file mode 100644 index 000000000..075d52b1d --- /dev/null +++ b/src/core/Elsa.Mediator/Middleware/Request/RequestPipelineBuilderExtensions.cs @@ -0,0 +1,10 @@ +using Elsa.Mediator.Middleware.Request.Components; +using Elsa.Mediator.Middleware.Request.Contracts; + +namespace Elsa.Mediator.Middleware.Request; + +public static class RequestPipelineBuilderExtensions +{ + public static IRequestPipelineBuilder UseRequestHandlers(this IRequestPipelineBuilder builder) => builder.UseMiddleware(); + public static IRequestPipelineBuilder UseRequestLogging(this IRequestPipelineBuilder builder) => builder.UseMiddleware(); +} \ No newline at end of file diff --git a/src/core/Elsa.Mediator/Services/DefaultMediator.cs b/src/core/Elsa.Mediator/Services/DefaultMediator.cs new file mode 100644 index 000000000..3943b1bdf --- /dev/null +++ b/src/core/Elsa.Mediator/Services/DefaultMediator.cs @@ -0,0 +1,57 @@ +using Elsa.Mediator.Contracts; +using Elsa.Mediator.Middleware.Command; +using Elsa.Mediator.Middleware.Command.Contracts; +using Elsa.Mediator.Middleware.Notification; +using Elsa.Mediator.Middleware.Notification.Contracts; +using Elsa.Mediator.Middleware.Request; +using Elsa.Mediator.Middleware.Request.Contracts; + +namespace Elsa.Mediator.Services; + +public class DefaultMediator : IMediator +{ + private readonly IRequestPipeline _requestPipeline; + private readonly ICommandPipeline _commandPipeline; + private readonly INotificationPipeline _notificationPipeline; + + public DefaultMediator( + IRequestPipeline requestPipeline, + ICommandPipeline commandPipeline, + INotificationPipeline notificationPipeline) + { + _requestPipeline = requestPipeline; + _commandPipeline = commandPipeline; + _notificationPipeline = notificationPipeline; + } + + public async Task RequestAsync(IRequest request, CancellationToken cancellationToken = default) + { + var responseType = typeof(T); + var context = new RequestContext(request, responseType, cancellationToken); + await _requestPipeline.ExecuteAsync(context); + + return (T)context.Response!; + } + + public async Task ExecuteAsync(ICommand command, CancellationToken cancellationToken = default) + { + var resultType = typeof(Unit); + var context = new CommandContext(command, resultType, cancellationToken); + await _commandPipeline.ExecuteAsync(context); + } + + public async Task ExecuteAsync(ICommand command, CancellationToken cancellationToken = default) + { + var resultType = typeof(T); + var context = new CommandContext(command, resultType, cancellationToken); + await _commandPipeline.ExecuteAsync(context); + + return (T)context.Result!; + } + + public async Task PublishAsync(INotification notification, CancellationToken cancellationToken = default) + { + var context = new NotificationContext(notification, cancellationToken); + await _notificationPipeline.ExecuteAsync(context); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Serialization/Elsa.Serialization.csproj b/src/core/Elsa.Serialization/Elsa.Serialization.csproj new file mode 100644 index 000000000..2bb3a4dfc --- /dev/null +++ b/src/core/Elsa.Serialization/Elsa.Serialization.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/core/Elsa/Elsa.csproj b/src/core/Elsa/Elsa.csproj new file mode 100644 index 000000000..d3640a17c --- /dev/null +++ b/src/core/Elsa/Elsa.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + Elsa + latest + + + + + + + + + + + + + + + + diff --git a/src/core/Elsa/Extensions/ServiceCollectionExtensions.cs b/src/core/Elsa/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..2cbc48d3a --- /dev/null +++ b/src/core/Elsa/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,78 @@ +using System; +using Elsa.ActivityNodeResolvers; +using Elsa.Contracts; +using Elsa.Dsl.Abstractions; +using Elsa.Dsl.Extensions; +using Elsa.Dsl.Services; +using Elsa.Expressions; +using Elsa.Options; +using Elsa.Persistence.Extensions; +using Elsa.Pipelines.ActivityExecution; +using Elsa.Pipelines.WorkflowExecution; +using Elsa.Runtime.Contracts; +using Elsa.Runtime.Extensions; +using Elsa.Runtime.HostedServices; +using Elsa.Runtime.Options; +using Elsa.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddElsa(this IServiceCollection services) => services + .AddElsaCore() + .AddElsaRuntime() + .AddDefaultExpressionHandlers(); + + public static IServiceCollection AddElsaCore(this IServiceCollection services) + { + services.AddOptions(); + + return services + + // Core. + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + + // DSL. + .AddDsl() + + // Expressions. + .AddSingleton() + .AddSingleton() + + // Pipelines. + .AddSingleton() + .AddSingleton() + + // Persistence services. + .AddPersistenceServices() + + // Logging + .AddLogging(); + } + + private static IServiceCollection AddDefaultExpressionHandlers(this IServiceCollection services) => + services + .AddExpressionHandler() + .AddExpressionHandler() + .AddExpressionHandler() + .AddExpressionHandler() + .AddExpressionHandler(); + + public static IServiceCollection AddWorkflowProvider(this IServiceCollection services) where T : class, IWorkflowProvider => services.AddSingleton(); + public static IServiceCollection AddStimulusHandler(this IServiceCollection services) where T : class, IStimulusHandler => services.AddSingleton(); + public static IServiceCollection AddInstructionInterpreter(this IServiceCollection services) where T : class, IWorkflowInstructionInterpreter => services.AddSingleton(); + public static IServiceCollection ConfigureWorkflowRuntime(this IServiceCollection services, Action configure) => services.Configure(configure); + public static IServiceCollection IndexWorkflowTriggers(this IServiceCollection services) => services.AddHostedService(); +} \ No newline at end of file diff --git a/src/core/Elsa/Extensions/ServiceProviderExtensions.cs b/src/core/Elsa/Extensions/ServiceProviderExtensions.cs new file mode 100644 index 000000000..7932a203b --- /dev/null +++ b/src/core/Elsa/Extensions/ServiceProviderExtensions.cs @@ -0,0 +1,22 @@ +using System; +using Elsa.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Extensions; + +public static class ServiceProviderExtensions +{ + public static IServiceProvider ConfigureDefaultActivityExecutionPipeline(this IServiceProvider services, Action setup) + { + var pipeline = services.GetRequiredService(); + pipeline.Setup(setup); + return services; + } + + public static IServiceProvider ConfigureDefaultWorkflowExecutionPipeline(this IServiceProvider services, Action setup) + { + var pipeline = services.GetRequiredService(); + pipeline.Setup(setup); + return services; + } +} \ No newline at end of file diff --git a/src/designer/elsa-workflows-designer/.editorconfig b/src/designer/elsa-workflows-designer/.editorconfig new file mode 100644 index 000000000..f1cc3ad32 --- /dev/null +++ b/src/designer/elsa-workflows-designer/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/src/designer/elsa-workflows-designer/.gitignore b/src/designer/elsa-workflows-designer/.gitignore new file mode 100644 index 000000000..c3ea58a61 --- /dev/null +++ b/src/designer/elsa-workflows-designer/.gitignore @@ -0,0 +1,26 @@ +dist/ +www/ +loader/ + +*~ +*.sw[mnpcod] +*.log +*.lock +*.tmp +*.tmp.* +log.txt +*.sublime-project +*.sublime-workspace + +.stencil/ +.idea/ +.vscode/ +.sass-cache/ +.versions/ +node_modules/ +$RECYCLE.BIN/ + +.DS_Store +Thumbs.db +UserInterfaceState.xcuserstate +.env diff --git a/src/designer/elsa-workflows-designer/.prettierrc.json b/src/designer/elsa-workflows-designer/.prettierrc.json new file mode 100644 index 000000000..7ca3a28a9 --- /dev/null +++ b/src/designer/elsa-workflows-designer/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "quoteProps": "consistent", + "printWidth": 180, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} diff --git a/src/designer/elsa-workflows-designer/LICENSE b/src/designer/elsa-workflows-designer/LICENSE new file mode 100644 index 000000000..b442934b7 --- /dev/null +++ b/src/designer/elsa-workflows-designer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/designer/elsa-workflows-designer/package-lock.json b/src/designer/elsa-workflows-designer/package-lock.json new file mode 100644 index 000000000..186859d16 --- /dev/null +++ b/src/designer/elsa-workflows-designer/package-lock.json @@ -0,0 +1,1181 @@ +{ + "name": "elsa-workflows-designer", + "version": "3.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@antv/x6": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@antv/x6/-/x6-1.29.1.tgz", + "integrity": "sha512-XSCtW1T/CIC0QUUUYW04EUQ5VnBMsShiNz9NP64oN8fAW14Jw99NKUhzToKpHGqQtVDg6qBX1UjFsfi0SUL4lg==", + "requires": { + "csstype": "^3.0.3", + "jquery": "^3.5.1", + "jquery-mousewheel": "^3.1.13", + "lodash-es": "^4.17.15", + "mousetrap": "^1.6.5", + "utility-types": "^3.10.0" + } + }, + "@babel/code-frame": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", + "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.15.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@fullhuman/postcss-purgecss": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-4.1.3.tgz", + "integrity": "sha512-jqcsyfvq09VOsMXxJMPLRF6Fhg/NNltzWKnC9qtzva+QKTxerCO4esG6je7hbnmkpZtaDyPTwMBj9bzfWorsrw==", + "dev": true, + "requires": { + "purgecss": "^4.1.3" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@stencil/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.12.0.tgz", + "integrity": "sha512-hQlQKh5CUJe8g3L5avLLsfgVu95HMS2LToTtS7gpvvP0eKes1VvAC56uhI+vH4u44GZl9ck/g1rJBVRmMWu0LA==" + }, + "@stencil/postcss": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stencil/postcss/-/postcss-2.1.0.tgz", + "integrity": "sha512-/e4TYEXErGaHxH0ocg620YqEMLuMLpK/Wg4MJsiJglrLZCZhU4XCX1N0SwxaIOUbEZ1Zh+AqQ++yMI92ilndEA==", + "dev": true, + "requires": { + "autoprefixer": "^10.3.6", + "postcss": "~8.3.8" + }, + "dependencies": { + "postcss": { + "version": "8.3.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", + "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "dev": true, + "requires": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^0.6.2" + } + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true + } + } + }, + "@stencil/sass": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-1.5.2.tgz", + "integrity": "sha512-nJ93pUSylsGsMX0eLmhxh1oEljcUjj5mYjhB9ziCdoaydAdjukrUoqDCC7tdVbOcBo2hKptQyWqCtETnBGYsXQ==", + "dev": true + }, + "@stencil/state-tunnel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stencil/state-tunnel/-/state-tunnel-1.0.1.tgz", + "integrity": "sha512-DYG8uROgL9hkjVTCtCfRBb0d3FwpiFB0muRrNZQ2X1Qo5hxMuNNji76/ILddqeq0AfgkKCW82xrMPDpy+rNIhQ==" + }, + "@stencil/store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@stencil/store/-/store-1.5.0.tgz", + "integrity": "sha512-fe5fCF6dgVlDM1iLRkkJUyUh0Tfx305asVGgMAJjIs7Q+x/b1pGgTLROm9Ibr53PZuFwr5Kg+4h9p4FLbYqHgA==" + }, + "@tailwindcss/forms": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.4.0.tgz", + "integrity": "sha512-DeaQBx6EgEeuZPQACvC+mKneJsD8am1uiJugjgQK1+/Vt+Ai0GpFBC2T2fqnUad71WgOxyrZPE6BG1VaI6YqfQ==", + "dev": true, + "requires": { + "mini-svg-data-uri": "^1.2.3" + } + }, + "@tailwindcss/typography": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.0.tgz", + "integrity": "sha512-1p/3C6C+JJziS/ghtG8ACYalbA2SyLJY27Pm33cVTlAoY6VQ7zfm2H64cPxUMBkVIlWXTtWHhZcZJPobMRmQAA==", + "dev": true, + "requires": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + } + }, + "@types/jquery": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.11.tgz", + "integrity": "sha512-lYZGdfOtUa0XFjIATQgiogqeTY5PNNMOmp3Jq48ghmJALL8t/IqABRqlEwdHfuUdA8iIE1uGD1HoI4a7Tiy6OA==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/jquery-mousewheel": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@types/jquery-mousewheel/-/jquery-mousewheel-3.1.9.tgz", + "integrity": "sha512-1lcbzFEFWF05Q66D6H0qH3cheI/KNJKpYmEknYSjzppRh2h4UPNsr+5TsEYSwzFg7ia37Dd5iEK3b03SS6OCjQ==", + "dev": true, + "requires": { + "@types/jquery": "*" + } + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.0.tgz", + "integrity": "sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA==", + "dev": true, + "requires": { + "browserslist": "^4.17.5", + "caniuse-lite": "^1.0.30001272", + "fraction.js": "^4.1.1", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.1.0" + } + }, + "axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "requires": { + "follow-redirects": "^1.14.4" + } + }, + "axios-middleware": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/axios-middleware/-/axios-middleware-0.3.1.tgz", + "integrity": "sha512-DuPLxZgqBYnRgEhEUulNzPspf0j9jlH5iWOJHEfuLFfsxWLc7vjeOxR903dp+G+nrU3TYWbg2COLZ4PDUwh87g==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", + "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001271", + "electron-to-chromium": "^1.3.878", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001272", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz", + "integrity": "sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "el-transition": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/el-transition/-/el-transition-0.0.7.tgz", + "integrity": "sha512-a23pEa7ahqLmBIEqtxi8CdL6CRLF52ZoAwawDFTD1IDkegrL4iValQAj6IMEgIEXPSrXZtx/Nr/ej6ebov4uoA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.883", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.883.tgz", + "integrity": "sha512-goyjNx4wB9j911PBteb+AXNbErug7rJVkmDXWdw5SCVn2JlARBwsqucPkvp1h5mXWxHUbBRK3bwXTrqSxSiAIQ==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "follow-redirects": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", + "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==" + }, + "fraction.js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "requires": { + "import-from": "^3.0.0" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "jquery-mousewheel": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz", + "integrity": "sha1-BvAzXxbjU6aV5yBr9QUDy1I6buU=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "lilconfig": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", + "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mini-svg-data-uri": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.3.tgz", + "integrity": "sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "dev": true + }, + "mousetrap": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", + "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==" + }, + "nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true + }, + "node-releases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "postcss": { + "version": "8.4.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", + "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", + "dev": true, + "requires": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.1" + } + }, + "postcss-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", + "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1", + "postcss": "^8.1.6" + } + }, + "postcss-load-config": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", + "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", + "dev": true, + "requires": { + "import-cwd": "^3.0.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, + "postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz", + "integrity": "sha512-D5PG53d209Z1Uhcc0qAZ5U3t5HagH3cxu+WLZ22jt3gLUpXM4eXXfiO14jiDWST3NNooX/E8wISfOhZ9eIjGTQ==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "purgecss": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", + "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", + "dev": true, + "requires": { + "commander": "^8.0.0", + "glob": "^7.1.7", + "postcss": "^8.3.5", + "postcss-selector-parser": "^6.0.6" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "source-map-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tailwindcss": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.7.tgz", + "integrity": "sha512-rZdKNHtC64jcQncLoWOuCzj4lQDTAgLtgK3WmQS88tTdpHh9OwLqULTQxI3tw9AMJsqSpCKlmcjW/8CSnni6zQ==", + "dev": true, + "requires": { + "arg": "^5.0.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "color-name": "^1.1.4", + "cosmiconfig": "^7.0.1", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "normalize-path": "^3.0.0", + "object-hash": "^2.2.0", + "postcss-js": "^3.0.3", + "postcss-load-config": "^3.1.0", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.7", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.20.0", + "tmp": "^0.2.1" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + } + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "typedi": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/typedi/-/typedi-0.10.0.tgz", + "integrity": "sha512-v3UJF8xm68BBj6AF4oQML3ikrfK2c9EmZUyLOfShpJuItAqVBHWP/KtpGinkSsIiP6EZyyb6Z3NXyW9dgS9X1w==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } +} diff --git a/src/designer/elsa-workflows-designer/package.json b/src/designer/elsa-workflows-designer/package.json new file mode 100644 index 000000000..8f55fc031 --- /dev/null +++ b/src/designer/elsa-workflows-designer/package.json @@ -0,0 +1,52 @@ +{ + "name": "elsa-workflows-designer", + "version": "3.0.0", + "description": "Stencil Component Starter", + "main": "dist/index.cjs.js", + "module": "dist/custom-elements/index.js", + "es2015": "dist/esm/index.mjs", + "es2017": "dist/esm/index.mjs", + "types": "dist/custom-elements/index.d.ts", + "collection": "dist/collection/collection-manifest.json", + "collection:main": "dist/collection/index.js", + "unpkg": "dist/elsa-workflows-designer/elsa-workflows-designer.esm.js", + "files": [ + "dist/", + "loader/" + ], + "scripts": { + "build": "stencil build", + "build.dev": "stencil build --dev", + "start": "stencil build --dev --watch --serve", + "test": "stencil test --spec --e2e", + "test.watch": "stencil test --spec --e2e --watchAll", + "generate": "stencil generate" + }, + "dependencies": { + "@antv/x6": "^1.29.1", + "@stencil/core": "^2.12.0", + "@stencil/state-tunnel": "^1.0.1", + "@stencil/store": "^1.5.0", + "axios": "^0.24.0", + "axios-middleware": "^0.3.1", + "reflect-metadata": "^0.1.13", + "typedi": "^0.10.0", + "lodash": "^4.17.21", + "uuid": "^8.3.2", + "el-transition": "^0.0.7", + "moment": "^2.29.1" + }, + "devDependencies": { + "@fullhuman/postcss-purgecss": "^4.1.3", + "@stencil/postcss": "^2.1.0", + "@stencil/sass": "^1.5.2", + "@tailwindcss/forms": "^0.4.0", + "@tailwindcss/typography": "^0.5.0", + "@types/jquery": "^3.5.11", + "@types/jquery-mousewheel": "^3.1.9", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.5", + "tailwindcss": "^3.0.7" + }, + "license": "MIT" +} diff --git a/src/designer/elsa-workflows-designer/readme.md b/src/designer/elsa-workflows-designer/readme.md new file mode 100644 index 000000000..3db484fb0 --- /dev/null +++ b/src/designer/elsa-workflows-designer/readme.md @@ -0,0 +1,75 @@ +![Built With Stencil](https://img.shields.io/badge/-Built%20With%20Stencil-16161d.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI%2BCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI%2BCgkuc3Qwe2ZpbGw6I0ZGRkZGRjt9Cjwvc3R5bGU%2BCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik00MjQuNywzNzMuOWMwLDM3LjYtNTUuMSw2OC42LTkyLjcsNjguNkgxODAuNGMtMzcuOSwwLTkyLjctMzAuNy05Mi43LTY4LjZ2LTMuNmgzMzYuOVYzNzMuOXoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTQyNC43LDI5Mi4xSDE4MC40Yy0zNy42LDAtOTIuNy0zMS05Mi43LTY4LjZ2LTMuNkgzMzJjMzcuNiwwLDkyLjcsMzEsOTIuNyw2OC42VjI5Mi4xeiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNDI0LjcsMTQxLjdIODcuN3YtMy42YzAtMzcuNiw1NC44LTY4LjYsOTIuNy02OC42SDMzMmMzNy45LDAsOTIuNywzMC43LDkyLjcsNjguNlYxNDEuN3oiLz4KPC9zdmc%2BCg%3D%3D&colorA=16161d&style=flat-square) + +# Stencil Component Starter + +This is a starter project for building a standalone Web Component using Stencil. + +Stencil is also great for building entire apps. For that, use the [stencil-app-starter](https://github.com/ionic-team/stencil-app-starter) instead. + +# Stencil + +Stencil is a compiler for building fast web apps using Web Components. + +Stencil combines the best concepts of the most popular frontend frameworks into a compile-time rather than run-time tool. Stencil takes TypeScript, JSX, a tiny virtual DOM layer, efficient one-way data binding, an asynchronous rendering pipeline (similar to React Fiber), and lazy-loading out of the box, and generates 100% standards-based Web Components that run in any browser supporting the Custom Elements v1 spec. + +Stencil components are just Web Components, so they work in any major framework or with no framework at all. + +## Getting Started + +To start building a new web component using Stencil, clone this repo to a new directory: + +```bash +git clone https://github.com/ionic-team/stencil-component-starter.git my-component +cd my-component +git remote rm origin +``` + +and run: + +```bash +npm install +npm start +``` + +To build the component for production, run: + +```bash +npm run build +``` + +To run the unit tests for the components, run: + +```bash +npm test +``` + +Need help? Check out our docs [here](https://stenciljs.com/docs/my-first-component). + + +## Naming Components + +When creating new component tags, we recommend _not_ using `stencil` in the component name (ex: ``). This is because the generated component has little to nothing to do with Stencil; it's just a web component! + +Instead, use a prefix that fits your company or any name for a group of related components. For example, all of the Ionic generated web components use the prefix `ion`. + + +## Using this component + +There are three strategies we recommend for using web components built with Stencil. + +The first step for all three of these strategies is to [publish to NPM](https://docs.npmjs.com/getting-started/publishing-npm-packages). + +### Script tag + +- Put a script tag similar to this `` in the head of your index.html +- Then you can use the element anywhere in your template, JSX, html etc + +### Node Modules +- Run `npm install my-component --save` +- Put a script tag similar to this `` in the head of your index.html +- Then you can use the element anywhere in your template, JSX, html etc + +### In a stencil-starter app +- Run `npm install my-component --save` +- Add an import to the npm packages `import my-component;` +- Then you can use the element anywhere in your template, JSX, html etc diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts new file mode 100644 index 000000000..0c09d918e --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -0,0 +1,527 @@ +/* eslint-disable */ +/* tslint:disable */ +/** + * This is an autogenerated file created by the Stencil compiler. + * It contains typing information for all components that exist in this project. + */ +import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; +import { ActionDefinition, ActionInvokedArgs, Activity, ActivityDescriptor, ActivitySelectedArgs, ContainerSelectedArgs, GraphUpdatedArgs, TabChangedArgs, TabDefinition, Trigger, TriggerDescriptor, Workflow, WorkflowInstance, WorkflowInstanceSummary, WorkflowSummary } from "./models"; +import { ActivityUpdatedArgs, DeleteActivityRequestedArgs } from "./components/designer/activity-properties-editor/activity-properties-editor"; +import { AddActivityArgs } from "./components/designer/canvas/canvas"; +import { NodeInputContext } from "./services/node-input-driver"; +import { MenuItem } from "./components/shared/context-menu/models"; +import { DropdownButtonItem, DropdownButtonOrigin } from "./components/shared/dropdown-button/models"; +import { AddActivityArgs as AddActivityArgs1 } from "./components/designer/canvas/canvas"; +import { PagerData } from "./components/shared/pager/pager"; +import { PanelPosition, PanelStateChangedArgs } from "./components/designer/panel/models"; +import { Graph } from "@antv/x6"; +import { TriggerDeselectedArgs, TriggerSelectedArgs, TriggersUpdatedArgs } from "./components/designer/trigger-container/trigger-container"; +import { DeleteTriggerRequestedArgs, TriggerUpdatedArgs } from "./components/designer/trigger-properties-editor/trigger-properties-editor"; +import { WorkflowUpdatedArgs } from "./components/designer/workflow-editor/workflow-editor"; +import { ActivityDriverRegistry } from "./services"; +import { WorkflowPropsUpdatedArgs } from "./components/designer/workflow-properties-editor/workflow-properties-editor"; +import { PublishClickedArgs } from "./components/toolbar/workflow-publish-button/workflow-publish-button"; +export namespace Components { + interface ElsaActivityPropertiesEditor { + "activity"?: Activity; + "activityDescriptors": Array; + "hide": () => Promise; + "show": () => Promise; + } + interface ElsaCanvas { + "addActivity": (args: AddActivityArgs) => Promise; + "exportGraph": () => Promise; + "importGraph": (root: Activity) => Promise; + "interactiveMode": boolean; + "updateLayout": () => Promise; + } + interface ElsaCheckListInput { + "inputContext": NodeInputContext; + } + interface ElsaContextMenu { + "menuItems": Array; + } + interface ElsaDropdownButton { + "icon"?: any; + "items": Array; + "origin": DropdownButtonOrigin; + "text": string; + } + interface ElsaDropdownInput { + "inputContext": NodeInputContext; + } + interface ElsaFlowchart { + "activityDescriptors": Array; + "addActivity": (args: AddActivityArgs) => Promise; + "exportRoot": () => Promise; + "importRoot": (root: Activity) => Promise; + "interactiveMode": boolean; + "root"?: Activity; + "updateLayout": () => Promise; + } + interface ElsaFormPanel { + "actions": Array; + "headerText": string; + "selectedTabIndex"?: number; + "tabs": Array; + } + interface ElsaModalDialog { + "actions": Array; + "hide": (animate?: boolean) => Promise; + "show": (animate?: boolean) => Promise; + "size": string; + } + interface ElsaPager { + "page": number; + "pageSize": number; + "totalCount": number; + } + interface ElsaPanel { + "position": PanelPosition; + } + interface ElsaSingleLineInput { + "inputContext": NodeInputContext; + } + interface ElsaSlideOverPanel { + "actions": Array; + "expand": boolean; + "headerText": string; + "hide": () => Promise; + "selectedTab"?: TabDefinition; + "show": () => Promise; + "tabs": Array; + } + interface ElsaStudio { + "serverUrl": string; + } + interface ElsaToolbox { + "graph": Graph; + } + interface ElsaToolboxActivities { + "activityDescriptors": Array; + "graph": Graph; + "triggerDescriptors": Array; + } + interface ElsaToolboxTriggers { + "activityDescriptors": Array; + "graph": Graph; + "triggerDescriptors": Array; + } + interface ElsaTriggerContainer { + "deselectAll": () => Promise; + "interactiveMode": boolean; + "triggerDescriptors": Array; + "triggers": Array; + } + interface ElsaTriggerPropertiesEditor { + "hide": () => Promise; + "show": () => Promise; + "trigger"?: Trigger; + "triggerDescriptors": Array; + } + interface ElsaWorkflowBrowser { + "hide": () => Promise; + "show": () => Promise; + } + interface ElsaWorkflowEditor { + "activityDescriptors": Array; + "getWorkflow": () => Promise; + "importWorkflow": (workflow: Workflow, workflowInstance?: WorkflowInstance) => Promise; + "importWorkflowMetadata": (workflow: Workflow) => Promise; + "registerActivityDrivers": (register: (registry: ActivityDriverRegistry) => void) => Promise; + "triggerDescriptors": Array; + } + interface ElsaWorkflowInstanceBrowser { + "hide": () => Promise; + "show": () => Promise; + } + interface ElsaWorkflowPropertiesEditor { + "hide": () => Promise; + "show": () => Promise; + "workflow"?: Workflow; + } + interface ElsaWorkflowPublishButton { + "publishing": boolean; + } + interface ElsaWorkflowToolbar { + } + interface ElsaWorkflowToolbarMenu { + } +} +declare global { + interface HTMLElsaActivityPropertiesEditorElement extends Components.ElsaActivityPropertiesEditor, HTMLStencilElement { + } + var HTMLElsaActivityPropertiesEditorElement: { + prototype: HTMLElsaActivityPropertiesEditorElement; + new (): HTMLElsaActivityPropertiesEditorElement; + }; + interface HTMLElsaCanvasElement extends Components.ElsaCanvas, HTMLStencilElement { + } + var HTMLElsaCanvasElement: { + prototype: HTMLElsaCanvasElement; + new (): HTMLElsaCanvasElement; + }; + interface HTMLElsaCheckListInputElement extends Components.ElsaCheckListInput, HTMLStencilElement { + } + var HTMLElsaCheckListInputElement: { + prototype: HTMLElsaCheckListInputElement; + new (): HTMLElsaCheckListInputElement; + }; + interface HTMLElsaContextMenuElement extends Components.ElsaContextMenu, HTMLStencilElement { + } + var HTMLElsaContextMenuElement: { + prototype: HTMLElsaContextMenuElement; + new (): HTMLElsaContextMenuElement; + }; + interface HTMLElsaDropdownButtonElement extends Components.ElsaDropdownButton, HTMLStencilElement { + } + var HTMLElsaDropdownButtonElement: { + prototype: HTMLElsaDropdownButtonElement; + new (): HTMLElsaDropdownButtonElement; + }; + interface HTMLElsaDropdownInputElement extends Components.ElsaDropdownInput, HTMLStencilElement { + } + var HTMLElsaDropdownInputElement: { + prototype: HTMLElsaDropdownInputElement; + new (): HTMLElsaDropdownInputElement; + }; + interface HTMLElsaFlowchartElement extends Components.ElsaFlowchart, HTMLStencilElement { + } + var HTMLElsaFlowchartElement: { + prototype: HTMLElsaFlowchartElement; + new (): HTMLElsaFlowchartElement; + }; + interface HTMLElsaFormPanelElement extends Components.ElsaFormPanel, HTMLStencilElement { + } + var HTMLElsaFormPanelElement: { + prototype: HTMLElsaFormPanelElement; + new (): HTMLElsaFormPanelElement; + }; + interface HTMLElsaModalDialogElement extends Components.ElsaModalDialog, HTMLStencilElement { + } + var HTMLElsaModalDialogElement: { + prototype: HTMLElsaModalDialogElement; + new (): HTMLElsaModalDialogElement; + }; + interface HTMLElsaPagerElement extends Components.ElsaPager, HTMLStencilElement { + } + var HTMLElsaPagerElement: { + prototype: HTMLElsaPagerElement; + new (): HTMLElsaPagerElement; + }; + interface HTMLElsaPanelElement extends Components.ElsaPanel, HTMLStencilElement { + } + var HTMLElsaPanelElement: { + prototype: HTMLElsaPanelElement; + new (): HTMLElsaPanelElement; + }; + interface HTMLElsaSingleLineInputElement extends Components.ElsaSingleLineInput, HTMLStencilElement { + } + var HTMLElsaSingleLineInputElement: { + prototype: HTMLElsaSingleLineInputElement; + new (): HTMLElsaSingleLineInputElement; + }; + interface HTMLElsaSlideOverPanelElement extends Components.ElsaSlideOverPanel, HTMLStencilElement { + } + var HTMLElsaSlideOverPanelElement: { + prototype: HTMLElsaSlideOverPanelElement; + new (): HTMLElsaSlideOverPanelElement; + }; + interface HTMLElsaStudioElement extends Components.ElsaStudio, HTMLStencilElement { + } + var HTMLElsaStudioElement: { + prototype: HTMLElsaStudioElement; + new (): HTMLElsaStudioElement; + }; + interface HTMLElsaToolboxElement extends Components.ElsaToolbox, HTMLStencilElement { + } + var HTMLElsaToolboxElement: { + prototype: HTMLElsaToolboxElement; + new (): HTMLElsaToolboxElement; + }; + interface HTMLElsaToolboxActivitiesElement extends Components.ElsaToolboxActivities, HTMLStencilElement { + } + var HTMLElsaToolboxActivitiesElement: { + prototype: HTMLElsaToolboxActivitiesElement; + new (): HTMLElsaToolboxActivitiesElement; + }; + interface HTMLElsaToolboxTriggersElement extends Components.ElsaToolboxTriggers, HTMLStencilElement { + } + var HTMLElsaToolboxTriggersElement: { + prototype: HTMLElsaToolboxTriggersElement; + new (): HTMLElsaToolboxTriggersElement; + }; + interface HTMLElsaTriggerContainerElement extends Components.ElsaTriggerContainer, HTMLStencilElement { + } + var HTMLElsaTriggerContainerElement: { + prototype: HTMLElsaTriggerContainerElement; + new (): HTMLElsaTriggerContainerElement; + }; + interface HTMLElsaTriggerPropertiesEditorElement extends Components.ElsaTriggerPropertiesEditor, HTMLStencilElement { + } + var HTMLElsaTriggerPropertiesEditorElement: { + prototype: HTMLElsaTriggerPropertiesEditorElement; + new (): HTMLElsaTriggerPropertiesEditorElement; + }; + interface HTMLElsaWorkflowBrowserElement extends Components.ElsaWorkflowBrowser, HTMLStencilElement { + } + var HTMLElsaWorkflowBrowserElement: { + prototype: HTMLElsaWorkflowBrowserElement; + new (): HTMLElsaWorkflowBrowserElement; + }; + interface HTMLElsaWorkflowEditorElement extends Components.ElsaWorkflowEditor, HTMLStencilElement { + } + var HTMLElsaWorkflowEditorElement: { + prototype: HTMLElsaWorkflowEditorElement; + new (): HTMLElsaWorkflowEditorElement; + }; + interface HTMLElsaWorkflowInstanceBrowserElement extends Components.ElsaWorkflowInstanceBrowser, HTMLStencilElement { + } + var HTMLElsaWorkflowInstanceBrowserElement: { + prototype: HTMLElsaWorkflowInstanceBrowserElement; + new (): HTMLElsaWorkflowInstanceBrowserElement; + }; + interface HTMLElsaWorkflowPropertiesEditorElement extends Components.ElsaWorkflowPropertiesEditor, HTMLStencilElement { + } + var HTMLElsaWorkflowPropertiesEditorElement: { + prototype: HTMLElsaWorkflowPropertiesEditorElement; + new (): HTMLElsaWorkflowPropertiesEditorElement; + }; + interface HTMLElsaWorkflowPublishButtonElement extends Components.ElsaWorkflowPublishButton, HTMLStencilElement { + } + var HTMLElsaWorkflowPublishButtonElement: { + prototype: HTMLElsaWorkflowPublishButtonElement; + new (): HTMLElsaWorkflowPublishButtonElement; + }; + interface HTMLElsaWorkflowToolbarElement extends Components.ElsaWorkflowToolbar, HTMLStencilElement { + } + var HTMLElsaWorkflowToolbarElement: { + prototype: HTMLElsaWorkflowToolbarElement; + new (): HTMLElsaWorkflowToolbarElement; + }; + interface HTMLElsaWorkflowToolbarMenuElement extends Components.ElsaWorkflowToolbarMenu, HTMLStencilElement { + } + var HTMLElsaWorkflowToolbarMenuElement: { + prototype: HTMLElsaWorkflowToolbarMenuElement; + new (): HTMLElsaWorkflowToolbarMenuElement; + }; + interface HTMLElementTagNameMap { + "elsa-activity-properties-editor": HTMLElsaActivityPropertiesEditorElement; + "elsa-canvas": HTMLElsaCanvasElement; + "elsa-check-list-input": HTMLElsaCheckListInputElement; + "elsa-context-menu": HTMLElsaContextMenuElement; + "elsa-dropdown-button": HTMLElsaDropdownButtonElement; + "elsa-dropdown-input": HTMLElsaDropdownInputElement; + "elsa-flowchart": HTMLElsaFlowchartElement; + "elsa-form-panel": HTMLElsaFormPanelElement; + "elsa-modal-dialog": HTMLElsaModalDialogElement; + "elsa-pager": HTMLElsaPagerElement; + "elsa-panel": HTMLElsaPanelElement; + "elsa-single-line-input": HTMLElsaSingleLineInputElement; + "elsa-slide-over-panel": HTMLElsaSlideOverPanelElement; + "elsa-studio": HTMLElsaStudioElement; + "elsa-toolbox": HTMLElsaToolboxElement; + "elsa-toolbox-activities": HTMLElsaToolboxActivitiesElement; + "elsa-toolbox-triggers": HTMLElsaToolboxTriggersElement; + "elsa-trigger-container": HTMLElsaTriggerContainerElement; + "elsa-trigger-properties-editor": HTMLElsaTriggerPropertiesEditorElement; + "elsa-workflow-browser": HTMLElsaWorkflowBrowserElement; + "elsa-workflow-editor": HTMLElsaWorkflowEditorElement; + "elsa-workflow-instance-browser": HTMLElsaWorkflowInstanceBrowserElement; + "elsa-workflow-properties-editor": HTMLElsaWorkflowPropertiesEditorElement; + "elsa-workflow-publish-button": HTMLElsaWorkflowPublishButtonElement; + "elsa-workflow-toolbar": HTMLElsaWorkflowToolbarElement; + "elsa-workflow-toolbar-menu": HTMLElsaWorkflowToolbarMenuElement; + } +} +declare namespace LocalJSX { + interface ElsaActivityPropertiesEditor { + "activity"?: Activity; + "activityDescriptors"?: Array; + "onActivityUpdated"?: (event: CustomEvent) => void; + "onDeleteActivityRequested"?: (event: CustomEvent) => void; + } + interface ElsaCanvas { + "interactiveMode"?: boolean; + } + interface ElsaCheckListInput { + "inputContext"?: NodeInputContext; + } + interface ElsaContextMenu { + "menuItems"?: Array; + } + interface ElsaDropdownButton { + "icon"?: any; + "items"?: Array; + "onItemSelected"?: (event: CustomEvent) => void; + "origin"?: DropdownButtonOrigin; + "text"?: string; + } + interface ElsaDropdownInput { + "inputContext"?: NodeInputContext; + } + interface ElsaFlowchart { + "activityDescriptors"?: Array; + "interactiveMode"?: boolean; + "onActivitySelected"?: (event: CustomEvent) => void; + "onContainerSelected"?: (event: CustomEvent) => void; + "onGraphUpdated"?: (event: CustomEvent) => void; + "root"?: Activity; + } + interface ElsaFormPanel { + "actions"?: Array; + "headerText"?: string; + "onActionInvoked"?: (event: CustomEvent) => void; + "onSelectedTabIndexChanged"?: (event: CustomEvent) => void; + "onSubmitted"?: (event: CustomEvent) => void; + "selectedTabIndex"?: number; + "tabs"?: Array; + } + interface ElsaModalDialog { + "actions"?: Array; + "onActionInvoked"?: (event: CustomEvent) => void; + "onHidden"?: (event: CustomEvent) => void; + "onShown"?: (event: CustomEvent) => void; + "size"?: string; + } + interface ElsaPager { + "onPaginated"?: (event: CustomEvent) => void; + "page"?: number; + "pageSize"?: number; + "totalCount"?: number; + } + interface ElsaPanel { + "onExpandedStateChanged"?: (event: CustomEvent) => void; + "position"?: PanelPosition; + } + interface ElsaSingleLineInput { + "inputContext"?: NodeInputContext; + } + interface ElsaSlideOverPanel { + "actions"?: Array; + "expand"?: boolean; + "headerText"?: string; + "onCollapsed"?: (event: CustomEvent) => void; + "onSubmitted"?: (event: CustomEvent) => void; + "selectedTab"?: TabDefinition; + "tabs"?: Array; + } + interface ElsaStudio { + "serverUrl"?: string; + } + interface ElsaToolbox { + "graph"?: Graph; + } + interface ElsaToolboxActivities { + "activityDescriptors"?: Array; + "graph"?: Graph; + "triggerDescriptors"?: Array; + } + interface ElsaToolboxTriggers { + "activityDescriptors"?: Array; + "graph"?: Graph; + "triggerDescriptors"?: Array; + } + interface ElsaTriggerContainer { + "interactiveMode"?: boolean; + "onTriggerDeselected"?: (event: CustomEvent) => void; + "onTriggerSelected"?: (event: CustomEvent) => void; + "onTriggersUpdated"?: (event: CustomEvent) => void; + "triggerDescriptors"?: Array; + "triggers"?: Array; + } + interface ElsaTriggerPropertiesEditor { + "onDeleteTriggerRequested"?: (event: CustomEvent) => void; + "onTriggerUpdated"?: (event: CustomEvent) => void; + "trigger"?: Trigger; + "triggerDescriptors"?: Array; + } + interface ElsaWorkflowBrowser { + "onWorkflowDefinitionSelected"?: (event: CustomEvent) => void; + } + interface ElsaWorkflowEditor { + "activityDescriptors"?: Array; + "onWorkflowUpdated"?: (event: CustomEvent) => void; + "triggerDescriptors"?: Array; + } + interface ElsaWorkflowInstanceBrowser { + "onWorkflowInstanceSelected"?: (event: CustomEvent) => void; + } + interface ElsaWorkflowPropertiesEditor { + "onWorkflowPropsUpdated"?: (event: CustomEvent) => void; + "workflow"?: Workflow; + } + interface ElsaWorkflowPublishButton { + "onExportClicked"?: (event: CustomEvent) => void; + "onImportClicked"?: (event: CustomEvent) => void; + "onNewClicked"?: (event: CustomEvent) => void; + "onPublishClicked"?: (event: CustomEvent) => void; + "onUnPublishClicked"?: (event: CustomEvent) => void; + "publishing"?: boolean; + } + interface ElsaWorkflowToolbar { + } + interface ElsaWorkflowToolbarMenu { + } + interface IntrinsicElements { + "elsa-activity-properties-editor": ElsaActivityPropertiesEditor; + "elsa-canvas": ElsaCanvas; + "elsa-check-list-input": ElsaCheckListInput; + "elsa-context-menu": ElsaContextMenu; + "elsa-dropdown-button": ElsaDropdownButton; + "elsa-dropdown-input": ElsaDropdownInput; + "elsa-flowchart": ElsaFlowchart; + "elsa-form-panel": ElsaFormPanel; + "elsa-modal-dialog": ElsaModalDialog; + "elsa-pager": ElsaPager; + "elsa-panel": ElsaPanel; + "elsa-single-line-input": ElsaSingleLineInput; + "elsa-slide-over-panel": ElsaSlideOverPanel; + "elsa-studio": ElsaStudio; + "elsa-toolbox": ElsaToolbox; + "elsa-toolbox-activities": ElsaToolboxActivities; + "elsa-toolbox-triggers": ElsaToolboxTriggers; + "elsa-trigger-container": ElsaTriggerContainer; + "elsa-trigger-properties-editor": ElsaTriggerPropertiesEditor; + "elsa-workflow-browser": ElsaWorkflowBrowser; + "elsa-workflow-editor": ElsaWorkflowEditor; + "elsa-workflow-instance-browser": ElsaWorkflowInstanceBrowser; + "elsa-workflow-properties-editor": ElsaWorkflowPropertiesEditor; + "elsa-workflow-publish-button": ElsaWorkflowPublishButton; + "elsa-workflow-toolbar": ElsaWorkflowToolbar; + "elsa-workflow-toolbar-menu": ElsaWorkflowToolbarMenu; + } +} +export { LocalJSX as JSX }; +declare module "@stencil/core" { + export namespace JSX { + interface IntrinsicElements { + "elsa-activity-properties-editor": LocalJSX.ElsaActivityPropertiesEditor & JSXBase.HTMLAttributes; + "elsa-canvas": LocalJSX.ElsaCanvas & JSXBase.HTMLAttributes; + "elsa-check-list-input": LocalJSX.ElsaCheckListInput & JSXBase.HTMLAttributes; + "elsa-context-menu": LocalJSX.ElsaContextMenu & JSXBase.HTMLAttributes; + "elsa-dropdown-button": LocalJSX.ElsaDropdownButton & JSXBase.HTMLAttributes; + "elsa-dropdown-input": LocalJSX.ElsaDropdownInput & JSXBase.HTMLAttributes; + "elsa-flowchart": LocalJSX.ElsaFlowchart & JSXBase.HTMLAttributes; + "elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes; + "elsa-modal-dialog": LocalJSX.ElsaModalDialog & JSXBase.HTMLAttributes; + "elsa-pager": LocalJSX.ElsaPager & JSXBase.HTMLAttributes; + "elsa-panel": LocalJSX.ElsaPanel & JSXBase.HTMLAttributes; + "elsa-single-line-input": LocalJSX.ElsaSingleLineInput & JSXBase.HTMLAttributes; + "elsa-slide-over-panel": LocalJSX.ElsaSlideOverPanel & JSXBase.HTMLAttributes; + "elsa-studio": LocalJSX.ElsaStudio & JSXBase.HTMLAttributes; + "elsa-toolbox": LocalJSX.ElsaToolbox & JSXBase.HTMLAttributes; + "elsa-toolbox-activities": LocalJSX.ElsaToolboxActivities & JSXBase.HTMLAttributes; + "elsa-toolbox-triggers": LocalJSX.ElsaToolboxTriggers & JSXBase.HTMLAttributes; + "elsa-trigger-container": LocalJSX.ElsaTriggerContainer & JSXBase.HTMLAttributes; + "elsa-trigger-properties-editor": LocalJSX.ElsaTriggerPropertiesEditor & JSXBase.HTMLAttributes; + "elsa-workflow-browser": LocalJSX.ElsaWorkflowBrowser & JSXBase.HTMLAttributes; + "elsa-workflow-editor": LocalJSX.ElsaWorkflowEditor & JSXBase.HTMLAttributes; + "elsa-workflow-instance-browser": LocalJSX.ElsaWorkflowInstanceBrowser & JSXBase.HTMLAttributes; + "elsa-workflow-properties-editor": LocalJSX.ElsaWorkflowPropertiesEditor & JSXBase.HTMLAttributes; + "elsa-workflow-publish-button": LocalJSX.ElsaWorkflowPublishButton & JSXBase.HTMLAttributes; + "elsa-workflow-toolbar": LocalJSX.ElsaWorkflowToolbar & JSXBase.HTMLAttributes; + "elsa-workflow-toolbar-menu": LocalJSX.ElsaWorkflowToolbarMenu & JSXBase.HTMLAttributes; + } + } +} diff --git a/src/designer/elsa-workflows-designer/src/components/activities/container-activity-component.tsx b/src/designer/elsa-workflows-designer/src/components/activities/container-activity-component.tsx new file mode 100644 index 000000000..0a8f9810c --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/container-activity-component.tsx @@ -0,0 +1,9 @@ +import {AddActivityArgs} from '../designer/canvas/canvas'; +import {Activity} from '../../models'; + +export interface ContainerActivityComponent { + updateLayout(): Promise; + addActivity(args: AddActivityArgs): Promise; + exportRoot(): Promise + importRoot(root: Activity): Promise; +} diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/activity-walker.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/activity-walker.ts new file mode 100644 index 000000000..977c98626 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/activity-walker.ts @@ -0,0 +1,80 @@ +import _, {camelCase} from "lodash"; +import {Activity, ActivityDescriptor, Container} from "../../../models"; + +export interface ActivityNode { + activity: Activity; + parents: Array; + children: Array; + port?: string; +} + +export interface ActivityPort { + activity: Activity; + port: string; +} + +export function walkActivities(root: Activity, descriptors: Array): ActivityNode { + const collectedActivities = new Set([root]); + const graph: ActivityNode = {activity: root, parents: [], children: []}; + const collectedNodes = new Set([graph]); + walkRecursive(graph, root, collectedActivities, collectedNodes, descriptors); + return graph; +} + +export function flatten(root: ActivityNode): Array { + return flattenList([root]); +} + +export function flattenList(activities: Array): Array { + const list: Array = [...activities]; + + for (const activity of activities) { + for (const child of activity.children) + list.push(child); + } + + return list; +} + +function walkRecursive(node: ActivityNode, activity: Activity, collectedActivities: Set, collectedNodes: Set, descriptors: Array) { + const ports = getPorts(node, activity, descriptors); + + for (const port of ports) { + const collectedNodesArray = Array.from(collectedNodes); + let childNode = collectedNodesArray.find(x => x.activity == port.activity); + + if (!childNode) { + childNode = {activity: port.activity, children: [], parents: [], port: port.port}; + collectedNodes.add(childNode); + } + + childNode.parents.push(node); + node.children.push(childNode); + collectedActivities.add(port.activity); + walkRecursive(childNode, port.activity, collectedActivities, collectedNodes, descriptors); + } +} + +function getPorts(node: ActivityNode, activity: Activity, descriptors: Array): Array { + const descriptor = descriptors.find(x => x.nodeType == activity.nodeType); + + if (!descriptor) + return []; + + let ports: Array = []; + + for (const outPort of descriptor.outPorts) { + const outPortName = _.camelCase(outPort.name); + const outbound: Activity | Array = activity[outPortName]; + + if (!!outbound) { + if (Array.isArray(outbound)) { + for (const a of outbound) + ports.push({activity: a, port: outPort.name}); + } else + ports.push({activity: outbound, port: outPort.name}); + } + } + + return ports; +} diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.scss b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.scss new file mode 100644 index 000000000..503f5eac1 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.scss @@ -0,0 +1,25 @@ +.x6-graph-scroller { + &.x6-graph-scroller-pannable[data-panning='false'] { + cursor: default; + } +} + +.x6-widget-selection-box { + box-sizing: content-box !important; + margin-top: -4px; + margin-left: -4px; + padding-right: 4px; + padding-bottom: 4px; + border: 2px dashed #34D399; + box-shadow: none; +} + +.x6-widget-selection-inner { + box-sizing: content-box !important; + margin-top: -8px; + margin-left: -8px; + padding-right: 12px; + padding-bottom: 12px; + border: 2px solid #34D399; + box-shadow: none; +} diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx new file mode 100644 index 000000000..27f2b123c --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx @@ -0,0 +1,324 @@ +import {Component, Element, Event, EventEmitter, h, Method, Prop, Watch} from '@stencil/core'; +import {Edge, Graph, Model, Node, NodeView} from '@antv/x6'; +import {v4 as uuid} from 'uuid'; +import {camelCase, first} from 'lodash'; +import './shapes'; +import './ports'; +import {ContainerActivityComponent} from '../container-activity-component'; +import {AddActivityArgs} from '../../designer/canvas/canvas'; +import {Activity, ActivityDescriptor, ActivitySelectedArgs, ContainerSelectedArgs, GraphUpdatedArgs} from '../../../models'; +import {createGraph} from './graph-factory'; +import {createNode} from './node-factory'; +import {Connection, Flowchart} from './models'; +import WorkflowEditorTunnel from '../../designer/state'; +import {ActivityNode, flattenList, walkActivities} from "./activity-walker"; +import PositionEventArgs = NodeView.PositionEventArgs; +import FromJSONData = Model.FromJSONData; + +@Component({ + tag: 'elsa-flowchart', + styleUrl: 'flowchart.scss', +}) +export class FlowchartComponent implements ContainerActivityComponent { + private rootId: string = uuid(); + + @Prop({mutable: true}) public activityDescriptors: Array = []; + @Prop({mutable: true}) public root?: Activity; + @Prop() public interactiveMode: boolean = true; + + @Element() el: HTMLElement; + container: HTMLElement; + graph: Graph; + target: Node; + + @Event() activitySelected: EventEmitter; + @Event() containerSelected: EventEmitter; + @Event() graphUpdated: EventEmitter; + + @Method() + public async updateLayout(): Promise { + const width = this.el.clientWidth; + const height = this.el.clientHeight; + this.graph.resize(width, height); + this.graph.updateBackground(); + } + + @Method() + public async addActivity(args: AddActivityArgs): Promise { + const graph = this.graph; + const {descriptor, x, y} = args; + + const activity: Activity = { + id: uuid(), + nodeType: descriptor.nodeType, + metadata: { + designer: { + position: { + x, + y + } + } + }, + }; + + const node = createNode(descriptor, activity, x, y); + graph.addNode(node); + } + + @Method() + public async exportRoot(): Promise { + return this.exportRootInternal(); + } + + @Method() + public async importRoot(root: Activity): Promise { + return this.importRootInternal(root); + } + + public async componentDidLoad() { + await this.createAndInitializeGraph(); + } + + public render() { + return ( +
this.container = el}/> + ); + } + + private createAndInitializeGraph = async () => { + const graph = this.graph = createGraph(this.container, { + nodeMovable: () => this.interactiveMode, + edgeMovable: () => this.interactiveMode, + arrowheadMovable: () => this.interactiveMode, + edgeLabelMovable: () => this.interactiveMode, + magnetConnectable: () => this.interactiveMode, + useEdgeTools: () => this.interactiveMode, + toolsAddable: () => this.interactiveMode, + stopDelegateOnDragging: () => this.interactiveMode, + vertexAddable: () => this.interactiveMode, + vertexDeletable: () => this.interactiveMode, + vertexMovable: () => this.interactiveMode, + }); + + graph.on('blank:click', this.onGraphClick); + graph.on('node:click', this.onNodeClick); + graph.on('edge:connected', this.onEdgeConnected); + graph.on('node:moved', this.onNodeMoved); + + graph.on('node:change:*', this.onGraphChanged); + graph.on('node:added', this.onGraphChanged); + graph.on('node:removed', this.onGraphChanged); + graph.on('edge:added', this.onGraphChanged); + graph.on('edge:removed', this.onGraphChanged); + graph.on('edge:connected', this.onGraphChanged); + + await this.updateLayout(); + } + + private exportRootInternal = (): Activity => { + const graph = this.graph; + const graphModel = graph.toJSON(); + const activities = graphModel.cells.filter(x => x.shape == 'activity').map(x => x.data as Activity); + const connections = graphModel.cells.filter(x => x.shape == 'elsa-edge' && !!x.data).map(x => x.data as Connection); + const remainingConnections: Array = []; // The connections remaining after transposition. + let remainingActivities: Array = [...activities]; // The activities remaining after transposition. + const activityDescriptors = this.activityDescriptors; + + // Transpose connections to activity outbound port properties. + for (const connection of connections) { + const source = activities.find(x => x.id == connection.source); + const target = activities.find(x => x.id == connection.target); + const sourceDescriptor = activityDescriptors.find(x => x.nodeType == source.nodeType); + const matchingTargetPort = sourceDescriptor.outPorts.find(x => x.name == connection.sourcePort); + + if (!!matchingTargetPort) { + // Assign the target activity directly to the outbound port of the source activity. + const outPortPropName = camelCase(connection.sourcePort); + source[outPortPropName] = target; + + // Remove the target activity from the list. + remainingActivities = remainingActivities.filter(x => x != target); + } else { + // Keep this connection. + remainingConnections.push(connection); + } + } + + return { + nodeType: 'Workflows.Flowchart', + activities: remainingActivities, + connections: remainingConnections, + id: this.rootId, + start: first(activities)?.id, + metadata: {}, + variables: [] + } as Flowchart; + } + + private importRootInternal = async (root: Activity) => { + this.rootId = root.id; + const descriptors = this.activityDescriptors; + const flowchart = root as Flowchart; + const flowchartGraph = walkActivities(root, this.activityDescriptors); + const flowchartNodes = flattenList(flowchartGraph.children); + const nodes: Array = []; + let edges: Array = []; + + // Create an X6 node for each activity. + for (const activityNode of flowchartNodes) { + const activity = activityNode.activity; + const position = activity.metadata.designer?.position || {x: 100, y: 100}; + const {x, y} = position; + const descriptor = descriptors.find(x => x.nodeType == activity.nodeType) + const node = createNode(descriptor, activity, x, y); + + nodes.push(node); + + // Create X6 edges for each child activity. + const childEdges = this.createEdges(activityNode); + edges = [...childEdges]; + } + + // Create X6 edges for each connection in the flowchart. + for (const connection of flowchart.connections) { + const edge: Edge.Metadata = this.createEdge(connection); + edges.push(edge); + } + + const model: FromJSONData = {nodes, edges}; + + // Freeze then unfreeze prevents an error from occurring when importing JSON a second time (e.g. after loading a new workflow. + this.graph.freeze(); + this.graph.fromJSON(model, {silent: false}); + this.graph.unfreeze(); + }; + + private createEdges = (activityNode: ActivityNode): Array => { + let edges: Array = []; + + for (const childNode of activityNode.children) { + const edge = this.createEdge({ + source: activityNode.activity.id, + sourcePort: activityNode.port, + target: childNode.activity.id, + targetPort: childNode.port + }); + + edges.push(edge); + } + + return edges; + } + + private createEdge = (connection: Connection): Edge.Metadata => { + return { + shape: 'elsa-edge', + zIndex: -1, + data: connection, + source: connection.source, + target: connection.target, + sourcePort: connection.sourcePort, + targetPort: connection.targetPort + }; + } + + private syncEdgeData = (cachedActivityId: string, updatedActivity: Activity) => { + const graph = this.graph; + const edges = graph.model.getEdges().filter(x => x.shape == 'elsa-edge' && !!x.data); + + for (const edge of edges) { + const connection: Connection = edge.data; + + if (connection.target != cachedActivityId && connection.source != cachedActivityId) + continue; + + if (connection.target == cachedActivityId) + connection.target = updatedActivity.id; + + if (connection.source == cachedActivityId) + connection.source = updatedActivity.id; + + edge.data = connection; + } + }; + + @Watch('root') + private async onRootChange(value: Activity) { + await this.importRootInternal(value); + } + + @Watch('interactiveMode') + private async onInteractiveModeChange(value: boolean) { + const graph = this.graph; + + if (!value) { + graph.disableSelectionMovable(); + graph.disableKeyboard(); + } else { + graph.enableSelectionMovable(); + graph.enableKeyboard(); + } + } + + private onGraphClick = async (e: PositionEventArgs) => this.containerSelected.emit({}); + + private onNodeClick = async (e: PositionEventArgs) => { + const node = e.node; + const activity = node.data as Activity; + const activityId = activity.id; + + const args: ActivitySelectedArgs = { + activity: activity, + applyChanges: a => { + node.data = a; + + // If the ID of the activity changed, we need to update connection references (X6 stores deep copies of data). + if (a.id !== activityId) + this.syncEdgeData(activityId, a); + }, + deleteActivity: a => node.remove({deep: true}) + }; + + this.activitySelected.emit(args); + }; + + private onNodeMoved = (e: PositionEventArgs) => { + const {node, x, y} = e; + const activity = node.data as Activity; + + activity.metadata = { + ...activity.metadata, + designer: { + position: { + x, + y + } + } + } + } + + private onEdgeConnected = (e: { isNew: boolean, edge: Edge }) => { + const edge = e.edge; + const sourceNode = edge.getSourceNode(); + const targetNode = edge.getTargetNode(); + const sourceActivity = edge.getSourceNode().data as Activity; + const targetActivity = targetNode.data as Activity; + const sourcePort = sourceNode.getPort(edge.getSourcePortId()).id; + const targetPort = targetNode.getPort(edge.getTargetPortId()).id; + + edge.data = { + source: sourceActivity.id, + sourcePort: sourcePort, + target: targetActivity.id, + targetPort: targetPort + } as Connection; + } + + private onGraphChanged = async (e: any) => { + this.graphUpdated.emit({exportGraph: this.exportRootInternal}); + } +} + +WorkflowEditorTunnel.injectProps(FlowchartComponent, ['activityDescriptors']); diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/graph-factory.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/graph-factory.ts new file mode 100644 index 000000000..923a180d5 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/graph-factory.ts @@ -0,0 +1,215 @@ +import {CellView, Graph, Node, Shape} from '@antv/x6'; +import './ports'; + +export function createGraph(container: HTMLElement, interacting: CellView.Interacting): Graph { + const graph = new Graph({ + container: container, + interacting: interacting, + grid: { + type: 'mesh', + size: 20, + visible: true, + args: { + color: '#e0e0e0' + } + }, + height: 5000, + width: 5000, + async: true, + autoResize: true, + keyboard: { + enabled: true, + global: false, + }, + clipboard: { + enabled: true, + useLocalStorage: true, + }, + selecting: { + enabled: true, + showNodeSelectionBox: true, + rubberband: true, + }, + scroller: { + enabled: true, + pannable: true, + pageVisible: false, + pageBreak: false, + padding: 0, + modifiers: ['ctrl', 'meta'], + }, + connecting: { + allowBlank: false, + allowMulti: true, + allowLoop: true, + allowNode: true, + allowEdge: false, + allowPort: true, + highlight: true, + //router: 'manhattan', + router: { + name: 'metro', + args: { + startDirections: ['bottom'], + endDirections: ['top'], + }, + }, + connector: 'elsa-connector', + // connector: { + // name: 'rounded', + // args: { + // radius: 20 + // }, + // }, + snap: { + radius: 20, + }, + validateMagnet({magnet}) { + return magnet.getAttribute('port-group') !== 'in' + }, + validateConnection({sourceView, targetView, sourceMagnet, targetMagnet}) { + if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { + return false + } + + if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') { + return false + } + + const portId = targetMagnet.getAttribute('port')! + const node = targetView.cell as Node + const port = node.getPort(portId) + return !(port && port.connected); + + }, + createEdge() { + return graph.createEdge({ + shape: 'elsa-edge', + zIndex: -1, + }) + } + }, + onPortRendered(args) { + const selectors = args.contentSelectors + const container = selectors && selectors.foContent + if (container) { + const port = document.createElement('div'); + port.className = 'rounded-full border border-2 border-blue h-8 w-8'; + port.innerHTML = `

done

`; + (container as HTMLElement).append(port); + } + }, + mousewheel: { + enabled: true, + modifiers: ['ctrl', 'meta'], + }, + history: { + enabled: true, + beforeAddCommand: (e: string, args: any) => { + + if (args.key == 'tools') + return false; + + const supportedEvents = ['cell:added', 'cell:removed', 'cell:change:*']; + + return supportedEvents.indexOf(e) >= 0; + }, + }, + // Todo: + // minimap: { + // enabled: true, + // container: this.container, + // }, + //interacting: () => state.interactingMap, + }); + + graph.on('edge:mouseenter', ({edge}) => { + edge.addTools([ + 'source-arrowhead', + 'target-arrowhead', + { + name: 'button-remove', + args: { + distance: -30, + }, + }, + ]) + }); + + graph.on('edge:mouseleave', ({edge}) => { + edge.removeTools() + }); + + graph.bindKey(['meta+c', 'ctrl+c'], () => { + const cells = graph.getSelectedCells() + if (cells.length) { + graph.copy(cells) + } + return false + }); + + graph.bindKey(['meta+x', 'ctrl+x'], () => { + const cells = graph.getSelectedCells() + if (cells.length) { + graph.cut(cells) + } + return false + }); + + graph.bindKey(['meta+v', 'ctrl+v'], () => { + if (!graph.isClipboardEmpty()) { + const cells = graph.paste({offset: 32}) + graph.cleanSelection() + graph.select(cells) + } + return false + }); + + //undo redo + graph.bindKey(['meta+z', 'ctrl+z'], () => { + if (graph.history.canUndo()) { + graph.history.undo() + } + return false + }); + + graph.bindKey(['meta+y', 'ctrl+y'], () => { + if (graph.history.canRedo()) { + graph.history.redo() + } + return false + }); + + // select all; + graph.bindKey(['meta+a', 'ctrl+a'], () => { + const nodes = graph.getNodes() + if (nodes) { + graph.select(nodes) + } + }); + + //delete + graph.bindKey('del', () => { + const cells = graph.getSelectedCells() + if (cells.length) { + graph.removeCells(cells) + } + }); + + // zoom + graph.bindKey(['ctrl+1', 'meta+1'], () => { + const zoom = graph.zoom() + if (zoom < 1.5) { + graph.zoom(0.1) + } + }); + + graph.bindKey(['ctrl+2', 'meta+2'], () => { + const zoom = graph.zoom() + if (zoom > 0.5) { + graph.zoom(-0.1) + } + }); + + return graph; +}; diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/models.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/models.ts new file mode 100644 index 000000000..d41f97db3 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/models.ts @@ -0,0 +1,13 @@ +import {Container} from '../../../models'; + +export interface Flowchart extends Container { + start: string; + connections: Array; +} + +export interface Connection { + source: string; + target: string; + sourcePort: string; + targetPort: string; +} diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/node-factory.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/node-factory.ts new file mode 100644 index 000000000..5d20e9429 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/node-factory.ts @@ -0,0 +1,53 @@ +import {Graph, Node} from '@antv/x6'; +import {Activity, ActivityDescriptor, Port} from '../../../models'; + +export function createNode(activityDescriptor: ActivityDescriptor, activity: Activity, x: number, y: number): Node.Metadata { + + let inPorts: Array = [...activityDescriptor.inPorts]; + let outPorts: Array = [...activityDescriptor.outPorts]; + + if (inPorts.length == 0) + inPorts = [{name: 'In', displayName: 'In'}]; + + if (inPorts.length == 1) + inPorts[0].displayName = null; + + if (outPorts.length == 0) + outPorts = [{name: 'Done', displayName: 'Done'}]; + + if (outPorts.length == 1) + outPorts[0].displayName = null; + + const inPortModels = inPorts.map(x => ({ + id: x.name, + group: 'in', + attrs: !!x.displayName ? { + text: { + text: x.displayName + } + } : null + })); + + const outPortModels = outPorts.map(x => ({ + id: x.name, + group: 'out', + attrs: { + text: { + text: x.displayName + } + } + })); + + const portModels = [...inPortModels, ...outPortModels]; + + return { + id: activity.id, + shape: 'activity', + activity: activity, + activityDescriptor: activityDescriptor, + x: x, + y: y, + data: activity, + ports: portModels + } as Node.Metadata; +} diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/ports.tsx b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/ports.tsx new file mode 100644 index 000000000..408599f33 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/ports.tsx @@ -0,0 +1,68 @@ +import {Graph, Line, Path} from "@antv/x6"; +import {toResult} from "@antv/x6/lib/registry/port-layout/util"; + +Graph.registerPortLayout('dynamicOut', (portsPositionArgs, elemBBox) => { + return portsPositionArgs.map((_, index) => { + + const portCount = portsPositionArgs.length; + const ratio = (index + 0.5) / portCount; + const p1 = portCount <= 3 ? elemBBox.getTopRight() : elemBBox.getBottomLeft(); + const p2 = portCount <= 3 ? elemBBox.getBottomRight() : elemBBox.getBottomRight(); + const line = new Line(p1, p2) + const p = line.pointAt(ratio); + + return toResult(p.round(), 0, {}); + + }); +}); + +Graph.registerPortLayout('dynamicIn', (portsPositionArgs, elemBBox) => { + return portsPositionArgs.map((_, index) => { + + const portCount = portsPositionArgs.length; + const ratio = (index + 0.5) / portCount; + const p1 = portCount <= 3 ? elemBBox.getTopLeft() : elemBBox.getTopLeft(); + const p2 = portCount <= 3 ? elemBBox.getBottomLeft() : elemBBox.getTopRight(); + const line = new Line(p1, p2) + const p = line.pointAt(ratio); + + return toResult(p.round(), 0, {}); + + }); +}); + +Graph.registerConnector( + 'elsa-connector', + (s, e) => { + const offset = 0; + const deltaY = Math.abs(e.y - s.y) + const control = Math.floor((deltaY / 3) * 2) + + const v1 = {x: s.x, y: s.y + offset + control} + const v2 = {x: e.x, y: e.y - offset - control} + + return Path.normalize( + `M ${s.x} ${s.y} + L ${s.x} ${s.y + offset} + C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset} + L ${e.x} ${e.y} + `, + ) + }, + true, +); + +Graph.registerEdge( + 'elsa-edge', + { + inherit: 'edge', + attrs: { + line: { + stroke: '#C2C8D5', + strokeWidth: 1, + targetMarker: null, + }, + }, + }, + true, +) diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/shapes.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/shapes.ts new file mode 100644 index 000000000..333a1c843 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/shapes.ts @@ -0,0 +1,138 @@ +import {Cell, Graph, Shape} from '@antv/x6'; +import {Container} from 'typedi'; +import {Activity, ActivityDescriptor} from '../../../models'; +import {ActivityDriverRegistry, ActivityDisplayContext} from '../../../services'; + +export class ActivityNode extends Shape.HTML { + get text() { + return this.store.get('text'); + } + + set text(value: string) { + this.store.set('text', value); + } + + get activity() { + return this.store.get('activity'); + } + + set activity(value: Activity) { + this.store.set('activity', value); + } + + get activityDescriptor() { + return this.store.get('activityDescriptor'); + } + + set activityDescriptor(value: ActivityDescriptor) { + this.store.set('activityDescriptor', value); + } + + init() { + super.init(); + this.updateSize(); + } + + setup() { + const self = this; + super.setup(); + this.on('change:text', this.updateSize, this); + + this.html = { + render() { + + return self.createHtml(); + + }, + shouldComponentUpdate(node: Cell) { + return node.hasChanged('text'); + }, + }; + } + + updateSize() { + const activityDescriptor = this.activityDescriptor as ActivityDescriptor; + + if (!activityDescriptor) + return; + + const wrapper = document.createElement('div'); + wrapper.className = 'w-full flex items-center pl-10 pr-2 py-2'; + wrapper.innerHTML = this.createHtml(); + document.body.append(wrapper); + const rect = wrapper.firstElementChild.getBoundingClientRect(); + wrapper.remove(); + + const width = rect.width; + const height = rect.height; + this.prop({size: {width, height}}); + } + + createHtml() { + const activityDescriptor = this.activityDescriptor as ActivityDescriptor; + const activity = this.activity as Activity; + const activityType = activityDescriptor.nodeType; + const driverRegistry = Container.get(ActivityDriverRegistry); + const driver = driverRegistry.createDriver(activityType); + + const displayContext: ActivityDisplayContext = { + activity: activity, + activityDescriptor: activityDescriptor, + displayType: "designer" + }; + + return driver.display(displayContext); + } +} + +ActivityNode.config({ + //portMarkup: [Markup.getForeignObjectMarkup()], + ports: { + groups: { + in: { + position: 'dynamicIn', + attrs: { + circle: { + r: 6, + magnet: true, + stroke: '#3c82f6', + strokeWidth: 2, + fill: '#fff', + }, + text: { + fontSize: 12, + fill: '#888', + }, + }, + label: { + position: { + name: 'outside', + }, + }, + }, + out: { + position: 'dynamicOut', + attrs: { + circle: { + r: 6, + magnet: true, + stroke: '#fff', + strokeWidth: 2, + fill: '#3c82f6', + }, + text: { + fontSize: 12, + fill: '#888', + }, + }, + label: { + position: { + name: 'outside', + }, + }, + }, + }, + }, +}); + +Graph.registerNode('activity', ActivityNode, true); diff --git a/src/designer/elsa-workflows-designer/src/components/designer/activity-properties-editor/activity-properties-editor.tsx b/src/designer/elsa-workflows-designer/src/components/designer/activity-properties-editor/activity-properties-editor.tsx new file mode 100644 index 000000000..57f8ca4b8 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/activity-properties-editor/activity-properties-editor.tsx @@ -0,0 +1,160 @@ +import {Component, Event, EventEmitter, h, Method, Prop, State} from '@stencil/core'; +import {camelCase} from 'lodash'; +import WorkflowEditorTunnel from '../state'; +import { + Activity, + ActivityDescriptor, + DefaultActions, InputDescriptor, + RenderNodePropContext, + RenderNodePropsContext, + TabChangedArgs, + TabDefinition +} from '../../../models'; +import {InputDriverRegistry} from "../../../services/input-driver-registry"; +import {Container} from "typedi"; +import {NodeInputContext} from "../../../services/node-input-driver"; +import {FormEntry} from "../../shared/forms/form-entry"; + +export interface ActivityUpdatedArgs { + activity: Activity; +} + +export interface DeleteActivityRequestedArgs { + activity: Activity; +} + +@Component({ + tag: 'elsa-activity-properties-editor', +}) +export class ActivityPropertiesEditor { + private slideOverPanel: HTMLElsaSlideOverPanelElement; + private renderContext: RenderNodePropsContext; + private readonly inputDriverRegistry: InputDriverRegistry; + + constructor() { + this.inputDriverRegistry = Container.get(InputDriverRegistry); + } + + @Prop({mutable: true}) public activityDescriptors: Array = []; + @Prop({mutable: true}) public activity?: Activity; + + @Event() public activityUpdated: EventEmitter; + @Event() public deleteActivityRequested: EventEmitter; + @State() private selectedTabIndex: number = 0; + + @Method() + public async show(): Promise { + await this.slideOverPanel.show(); + } + + @Method() + public async hide(): Promise { + await this.slideOverPanel.hide(); + } + + public componentWillRender() { + const activity = this.activity; + const activityDescriptor = this.findActivityDescriptor(); + const title = activityDescriptor?.displayName ?? activityDescriptor?.nodeType ?? 'Unknown Activity'; + const driverRegistry = this.inputDriverRegistry; + + const renderPropertyContexts: Array = activityDescriptor.inputProperties.map(inputDescriptor => { + const renderInputContext: NodeInputContext = { + node: activity, + nodeDescriptor: activityDescriptor, + inputDescriptor, + inputChanged: (v, s) => this.onPropertyEditorChanged(inputDescriptor, v, s) + }; + + const driver = driverRegistry.get(renderInputContext); + const control = driver?.renderInput(renderInputContext); + + return { + inputContext: renderInputContext, + inputControl: control, + } + }); + + this.renderContext = { + node: activity, + nodeDescriptor: activityDescriptor, + title, + properties: renderPropertyContexts + } + } + + public render() { + const {nodeDescriptor, title} = this.renderContext; + + const propertiesTab: TabDefinition = { + displayText: 'Properties', + content: () => this.renderPropertiesTab() + }; + + const commonTab: TabDefinition = { + displayText: 'Common', + content: () => this.renderCommonTab() + }; + + const tabs = !!nodeDescriptor ? [propertiesTab, commonTab] : []; + const actions = [DefaultActions.Delete(this.onDeleteActivity)]; + + return ( + this.onSelectedTabIndexChanged(e)} + actions={actions}/> + ); + } + + private findActivityDescriptor = (): ActivityDescriptor => !!this.activity ? this.activityDescriptors.find(x => x.nodeType == this.activity.nodeType) : null; + private onSelectedTabIndexChanged = (e: CustomEvent) => this.selectedTabIndex = e.detail.selectedTabIndex + + private onActivityIdChanged = (e: any) => { + const activity = this.activity; + const inputElement = e.target as HTMLInputElement; + + activity.id = inputElement.value; + this.activityUpdated.emit({activity: activity}); + } + + private onPropertyEditorChanged = (inputDescriptor: InputDescriptor, propertyValue: any, syntax: string) => { + const activity = this.activity; + const propertyName = inputDescriptor.name; + const camelCasePropertyName = camelCase(propertyName); + + activity[camelCasePropertyName] = { + type: inputDescriptor.type, + expression: { + type: syntax, + value: propertyValue + } + }; + + this.activityUpdated.emit({activity: activity}); + } + + private onDeleteActivity = () => this.deleteActivityRequested.emit({activity: this.activity}); + + private renderPropertiesTab = () => { + const {node, properties} = this.renderContext; + const activityId = node.id; + + return
+ + this.onActivityIdChanged(e)}/> + + + {properties.filter(x => !!x.inputControl).map(propertyContext => { + return propertyContext.inputControl; + })} +
+ }; + + private renderCommonTab = () => { + return
+
+ }; +} + +WorkflowEditorTunnel.injectProps(ActivityPropertiesEditor, ['activityDescriptors']); diff --git a/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.scss b/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.scss new file mode 100644 index 000000000..503f5eac1 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.scss @@ -0,0 +1,25 @@ +.x6-graph-scroller { + &.x6-graph-scroller-pannable[data-panning='false'] { + cursor: default; + } +} + +.x6-widget-selection-box { + box-sizing: content-box !important; + margin-top: -4px; + margin-left: -4px; + padding-right: 4px; + padding-bottom: 4px; + border: 2px dashed #34D399; + box-shadow: none; +} + +.x6-widget-selection-inner { + box-sizing: content-box !important; + margin-top: -8px; + margin-left: -8px; + padding-right: 12px; + padding-bottom: 12px; + border: 2px solid #34D399; + box-shadow: none; +} diff --git a/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.tsx b/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.tsx new file mode 100644 index 000000000..c72ac3a2e --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.tsx @@ -0,0 +1,48 @@ +import {Component, h, Method, Prop} from '@stencil/core'; +import {ContainerActivityComponent} from '../../activities/container-activity-component'; +import {Activity, ActivityDescriptor} from '../../../models'; + +export interface AddActivityArgs { + descriptor: ActivityDescriptor; + x?: number; + y?: number; +} + +@Component({ + tag: 'elsa-canvas', + styleUrl: 'canvas.scss', +}) +export class Canvas { + + private root: ContainerActivityComponent; + + @Prop() public interactiveMode: boolean = true; + + @Method() + public async addActivity(args: AddActivityArgs): Promise { + await this.root.addActivity(args); + } + + @Method() + public async updateLayout(): Promise { + await this.root.updateLayout(); + } + + @Method() + public async exportGraph(): Promise { + return await this.root.exportRoot(); + } + + @Method() + public async importGraph(root: Activity): Promise { + return await this.root.importRoot(root); + } + + render() { + return ( +
+ this.root = el} interactiveMode={this.interactiveMode} class="absolute left-0 top-0 right-0 bottom-0"/> +
+ ); + } +} diff --git a/src/designer/elsa-workflows-designer/src/components/designer/form-panel/form-panel.tsx b/src/designer/elsa-workflows-designer/src/components/designer/form-panel/form-panel.tsx new file mode 100644 index 000000000..bc722a759 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/form-panel/form-panel.tsx @@ -0,0 +1,114 @@ +import {Component, Event, EventEmitter, h, Prop} from '@stencil/core'; +import {ActionDefinition, ActionInvokedArgs, ActionType, TabChangedArgs, TabDefinition} from '../../../models'; + +@Component({ + tag: 'elsa-form-panel' +}) +export class FormPanel { + private formElement: HTMLFormElement; + + @Prop() public headerText: string; + @Prop() public tabs: Array = []; + @Prop({mutable: true}) public selectedTabIndex?: number; + @Prop() public actions: Array = []; + + @Event() public submitted: EventEmitter; + @Event() public selectedTabIndexChanged: EventEmitter; + @Event() public actionInvoked: EventEmitter; + + public render() { + return this.renderPanel(); + } + + private onTabClick(e: Event, tab: TabDefinition) { + e.preventDefault(); + const selectedTabIndex = this.selectedTabIndex = this.tabs.findIndex(x => tab == x); + this.selectedTabIndexChanged.emit({selectedTabIndex: selectedTabIndex}); + } + + private onSubmit(e: Event) { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + this.submitted.emit(formData); + } + + private renderPanel() { + const tabs = this.tabs; + const selectedTabIndex = this.selectedTabIndex; + const actions = this.actions; + + return ( +
+
this.onSubmit(e)} + ref={el => this.formElement = el} method="post"> +
+ +
+
+
+

+ {this.headerText} +

+
+
+
+ +
+ +
+ +
+ +
+ {tabs.map((tab, tabIndex) => { + const cssClass = tabIndex == selectedTabIndex ? '' : 'hidden'; + return
+ {tab.content()} +
+ })} +
+
+
+ +
+
+ {actions.map(action => { + + if (action.display) + return action.display(action); + + const cssClass = action.isPrimary + ? 'text-white bg-blue-600 hover:bg-blue-700 border-transparent focus:ring-blue-500' + : action.isDangerous ? 'text-white bg-red-600 hover:bg-red-700 border-transparent focus:ring-red-500' + : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-blue-500'; + + const buttonType = action.type == ActionType.Submit ? 'submit' : 'button'; + + const cancelHandler = () => { + }; + + const defaultHandler = (e: any, action: ActionDefinition) => this.actionInvoked.emit({action: action}); + const clickHandler = !!action.onClick ? action.onClick : action.type == ActionType.Cancel ? cancelHandler : defaultHandler; + + return + })} +
+
+
+
+ ); + } +} diff --git a/src/designer/elsa-workflows-designer/src/components/designer/panel/models.ts b/src/designer/elsa-workflows-designer/src/components/designer/panel/models.ts new file mode 100644 index 000000000..912ba24f1 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/panel/models.ts @@ -0,0 +1,10 @@ +export interface PanelStateChangedArgs { + expanded: boolean; +} + +export enum PanelPosition { + Left, + Top, + Right, + Bottom +} diff --git a/src/designer/elsa-workflows-designer/src/components/designer/panel/panel.scss b/src/designer/elsa-workflows-designer/src/components/designer/panel/panel.scss new file mode 100644 index 000000000..294867193 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/panel/panel.scss @@ -0,0 +1,19 @@ +.panel-state-closed { + + .panel { + + &panel-position-top { + width: 0; + } + + &panel-position-left { + height: 0; + } + } + + .panel-content-container { + left: 0; + top: 0; + display: none; + } +} diff --git a/src/designer/elsa-workflows-designer/src/components/designer/panel/panel.tsx b/src/designer/elsa-workflows-designer/src/components/designer/panel/panel.tsx new file mode 100644 index 000000000..49743824b --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/panel/panel.tsx @@ -0,0 +1,55 @@ +import {Component, Event, EventEmitter, h, Host, Prop, State} from '@stencil/core'; +import {PanelPosition, PanelStateChangedArgs} from './models'; + +@Component({ + tag: 'elsa-panel', + styleUrl: 'panel.scss', +}) +export class Panel { + @Prop() position: PanelPosition = PanelPosition.Left; + @Event() expandedStateChanged: EventEmitter; + @State() isExpanded: boolean = true; + + private onToggleClick = () => { + this.isExpanded = !this.isExpanded; + this.expandedStateChanged.emit({expanded: this.isExpanded}); + }; + + render() { + const isExpanded = this.isExpanded; + const position = this.position; + const stateClass = isExpanded ? 'panel-state-expanded' : 'panel-state-collapsed'; + + const containerClassMap = []; + containerClassMap[PanelPosition.Left] = 'panel-position-left left-0 top-0 bottom-0 border-r'; + containerClassMap[PanelPosition.Top] = 'panel-position-top left-0 top-0 right-0 border-b'; + containerClassMap[PanelPosition.Right] = 'panel-position-right right-0 top-0 bottom-0 border-l'; + const containerCssClass = containerClassMap[position]; + + const toggleClassMap = {}; + toggleClassMap[PanelPosition.Left] = 'panel-toggle-left'; + toggleClassMap[PanelPosition.Top] = 'panel-toggle-top'; + toggleClassMap[PanelPosition.Right] = 'panel-toggle-right'; + toggleClassMap[PanelPosition.Bottom] = 'panel-toggle-bottom'; + + const toggleCssClass = toggleClassMap[position]; + + return ( + + +
+ +
+ +
this.onToggleClick()}> + + + + +
+
+ ); + } +} diff --git a/src/designer/elsa-workflows-designer/src/components/designer/slide-over-panel/slide-over-panel.tsx b/src/designer/elsa-workflows-designer/src/components/designer/slide-over-panel/slide-over-panel.tsx new file mode 100644 index 000000000..a05e8b8c5 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/designer/slide-over-panel/slide-over-panel.tsx @@ -0,0 +1,194 @@ +import {Component, Event, EventEmitter, h, Method, Prop, State, Watch} from '@stencil/core'; +import {ActionDefinition, ActionType, TabDefinition} from '../../../models'; + +@Component({ + tag: 'elsa-slide-over-panel' +}) +export class SlideOverPanel { + private overlayElement: HTMLElement; + private formElement: HTMLFormElement; + + @Prop() public headerText: string; + @Prop() public tabs: Array = []; + @Prop({mutable: true}) public selectedTab?: TabDefinition; + @Prop() public actions: Array = []; + @Prop() public expand: boolean; + + @Event() public collapsed: EventEmitter; + @Event() public submitted: EventEmitter; + + + @Method() + public async show(): Promise { + this.isShowing = true; + this.isHiding = false; + this.isVisible = true; + } + + @Method() + public async hide(): Promise { + this.isHiding = true; + this.isShowing = false; + } + + @State() public isHiding: boolean = false; + @State() public isShowing: boolean = false; + @State() public isVisible: boolean = false; + + @Watch('expand') + private handleExpanded(value: boolean) { + this.isShowing = value; + this.isHiding = !value; + + if (value) + this.isVisible = true; + } + + public render() { + return this.renderPanel(); + } + + private onCloseClick = async () => { + await this.hide(); + }; + + private onOverlayClick = async (e: MouseEvent) => { + if (e.target != this.overlayElement) + return; + + // Hide panel. + await this.hide(); + + // Raise Form Submitted event to apply changes. + const formData = new FormData(this.formElement); + + this.submitted.emit(formData); + }; + + private onTransitionEnd = (e: TransitionEvent) => { + if (this.isHiding) { + this.isVisible = false; + this.isHiding = false; + this.collapsed.emit(); + } + }; + + private onTabClick(e: Event, tab: TabDefinition) { + e.preventDefault(); + this.selectedTab = tab; + } + + private onSubmit(e: Event) { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + this.submitted.emit(formData); + } + + private renderPanel() { + const isVisible = this.isVisible; + const isHiding = this.isHiding; + const wrapperClass = isVisible ? 'block' : 'hidden'; + const backdropClass = !isHiding && isVisible ? 'opacity-50' : 'opacity-0'; + const panelClass = !isHiding && isVisible ? 'max-w-2xl w-2xl' : 'max-w-0 w-0'; + const tabs = this.tabs; + const selectedTab = this.selectedTab; + const actions = this.actions; + + return ( +