Add project files.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using Elsa.Studio.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Elsa.Studio.Branding;
|
||||
|
||||
/// <summary>
|
||||
/// Provides default branding information for the application, including the application name, logo URL, and reverse logo URL.
|
||||
/// Implements the <see cref="Elsa.Studio.Branding.IBrandingProvider"/> interface.
|
||||
/// </summary>
|
||||
public class DefaultBrandingProvider : IBrandingProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual string AppName => $"Elsa {ToolVersion.GetDisplayVersion()}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string LogoUrl => GetLogoUrl(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string LogoReverseUrl => GetLogoUrl(true);
|
||||
|
||||
private string GetLogoUrl(bool v)
|
||||
{
|
||||
return "_content/Elsa.Studio.Shell/img/icon.png";
|
||||
}
|
||||
|
||||
public virtual RenderFragment Branding =>
|
||||
builder =>
|
||||
{
|
||||
builder.OpenComponent(0, typeof(DefaultBranding));
|
||||
builder.AddAttribute(1, "Provider", this);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Elsa.Studio.Branding;
|
||||
|
||||
/// <summary>
|
||||
/// Provides branding information for an application, including its name, logo, and reverse logo.
|
||||
/// </summary>
|
||||
public interface IBrandingProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the application.
|
||||
/// </summary>
|
||||
[Obsolete("Use Branding instead to provide a custom component, or configure the DefaultBrandingProvider with an AppName.")] string AppName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo on a white background.
|
||||
/// </summary>
|
||||
[Obsolete("Use Branding instead to provide a custom component, or configure the DefaultBrandingProvider with a LogoUrl.")] string? LogoUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo on a dark background
|
||||
/// </summary>
|
||||
[Obsolete("Use Branding instead to provide a custom component, or configure the DefaultBrandingProvider with a LogoReverseUrl.")] string? LogoReverseUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The component to render. Use this to completely customize the branding component.
|
||||
/// </summary>
|
||||
RenderFragment Branding { get; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@using Elsa.Studio.Contracts
|
||||
|
||||
<MudText Typo="Typo.body2">Client version: @PackageVersion</MudText>
|
||||
|
||||
@code {
|
||||
[Inject] private IClientInformationProvider ClientInformationProvider { get; set; } = default!;
|
||||
private string PackageVersion { get; set; } = "Loading...";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var info = await ClientInformationProvider.GetInfoAsync();
|
||||
PackageVersion = info.PackageVersion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@using Elsa.Studio.DomInterop.Contracts
|
||||
@using Elsa.Studio.Localization.Time.Components
|
||||
@using Elsa.Studio.Models
|
||||
|
||||
<MudSimpleTable Outlined="true" Striped="false" Dense="true" Elevation="0" Bordered="false">
|
||||
<tbody>
|
||||
@{
|
||||
var data = HideEmptyValues
|
||||
? Data.Where(x => !string.IsNullOrWhiteSpace(x.Text)).ToList()
|
||||
: Data;
|
||||
}
|
||||
@if (data.Any())
|
||||
{
|
||||
@foreach (var item in data)
|
||||
{
|
||||
<tr>
|
||||
<td style="width: 200px;">@item.Label</td>
|
||||
<td style="width: 50px;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ContentCopy" Size="Size.Small" Title="Copy" OnClick="@(() => OnCopyClicked(item.Text!))" Disabled="@(string.IsNullOrWhiteSpace(item.Text))"/>
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrWhiteSpace(item.Link))
|
||||
{
|
||||
<MudLink Typo="Typo.body2" Href="@item.Link">@item.Text</MudLink>
|
||||
}
|
||||
else if (item.OnClick != null)
|
||||
{
|
||||
<MudLink Typo="Typo.body2" OnClick="@item.OnClick">@item.Text</MudLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (item.Label == "Created" || item.Label == "Updated" || item.Label == "Finished")
|
||||
{
|
||||
<span><Timestamp Value="@Convert.ToDateTime(item.Text)"></Timestamp></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@item.Text</span>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShowNoDataAlert)
|
||||
{
|
||||
<MudAlert Severity="Severity.Normal" Dense="true" Variant="Variant.Text">@NoDataMessage</MudAlert>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@code {
|
||||
|
||||
/// <summary>
|
||||
/// The data to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public DataPanelModel Data { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// If true, empty values will be hidden.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool HideEmptyValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, a message will be displayed when there is no data.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowNoDataAlert { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message to display when there is no data.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string NoDataMessage { get; set; } = "No data available.";
|
||||
|
||||
[Inject] private IClipboard Clipboard { get; set; } = null!;
|
||||
|
||||
private async Task OnCopyClicked(string value)
|
||||
{
|
||||
await Clipboard.CopyText(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@using Elsa.Studio.Branding
|
||||
|
||||
<MudStack>
|
||||
<div class="d-flex gap-2 align-center">
|
||||
<MudImage Src="@Provider.LogoUrl" ObjectFit="ObjectFit.ScaleDown" Fluid="true" Width="32"
|
||||
Height="32"></MudImage>
|
||||
<MudText Typo="Typo.h6">@Provider.AppName </MudText>
|
||||
</div>
|
||||
</MudStack>
|
||||
|
||||
@code{
|
||||
[Parameter] public IBrandingProvider Provider { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@using Elsa.Api.Client.Resources.Scripting.Extensions;
|
||||
@using Elsa.Api.Client.Resources.Scripting.Models;
|
||||
@using Variant = MudBlazor.Variant
|
||||
|
||||
<div class="d-flex flex-grow-1 gap-4 align-content-start relative">
|
||||
<div class="flex-1">
|
||||
|
||||
<MudField Variant="Variant.Outlined" Label="@DisplayName" HelperText="@Description" Margin="Margin.Dense"
|
||||
Class="@(DisplayCodeEditor ? "d-block" : "d-none")" Disabled="ReadOnly">
|
||||
<StandaloneCodeEditor
|
||||
@ref="_monacoEditor"
|
||||
Id="@_monacoEditorId"
|
||||
ConstructionOptions="ConfigureMonacoEditor"
|
||||
CssClass="studio-expression-input-monaco-editor"
|
||||
OnDidInit="OnMonacoInitializedAsync"
|
||||
OnDidChangeModelContent="OnMonacoContentChangedAsync"/>
|
||||
</MudField>
|
||||
|
||||
@if (!DisplayCodeEditor)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
|
||||
</div>
|
||||
@{
|
||||
|
||||
RenderFragment DisplayDropdownItem(ExpressionDescriptor? item) => @<div>
|
||||
@{
|
||||
var isChecked = item?.Type == _selectedExpressionType;
|
||||
var iconColor = isChecked ? Color.Secondary : Color.Transparent;
|
||||
var displayName = item?.DisplayName ?? DefaultOption;
|
||||
|
||||
<MudMenuItem Class="studio-expression-input-menu-item"
|
||||
OnClick="@(() => OnSyntaxSelectedAsync(item?.Type))" Icon="@Icons.Material.Filled.Check"
|
||||
IconSize="Size.Small" IconColor="iconColor">@displayName</MudMenuItem>
|
||||
}
|
||||
</div>;
|
||||
|
||||
<div class="flex-none pt-1">
|
||||
<MudMenu Label="@ButtonLabel" Icon="@ButtonIcon" Variant="@ButtonVariant" Color="@ButtonColor"
|
||||
EndIcon="@ButtonEndIcon" IconColor="@ButtonEndColor" Dense="true" Disabled="ReadOnly">
|
||||
@DisplayDropdownItem(null)
|
||||
<MudDivider/>
|
||||
@foreach (var item in BrowsableExpressionDescriptors.Where(i => !string.IsNullOrEmpty(i.GetMonacoLanguage())))
|
||||
{
|
||||
@DisplayDropdownItem(item)
|
||||
}
|
||||
<MudDivider/>
|
||||
@foreach (var item in BrowsableExpressionDescriptors.Skip(1).Where(i => string.IsNullOrEmpty(i.GetMonacoLanguage())))
|
||||
{
|
||||
@DisplayDropdownItem(item)
|
||||
}
|
||||
</MudMenu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,231 @@
|
||||
using BlazorMonaco.Editor;
|
||||
using Elsa.Api.Client.Resources.Scripting.Extensions;
|
||||
using Elsa.Api.Client.Resources.Scripting.Models;
|
||||
using Elsa.Studio.Contracts;
|
||||
using Elsa.Studio.Extensions;
|
||||
using Elsa.Studio.Models;
|
||||
using Elsa.Studio.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using ThrottleDebounce;
|
||||
|
||||
namespace Elsa.Studio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A component that renders an input for an expression.
|
||||
/// </summary>
|
||||
public partial class ExpressionEditor : IDisposable
|
||||
{
|
||||
private readonly string[] _expressionTypeBlacklist = ["Literal", "Object", "Variable"];
|
||||
private string? _selectedExpressionType;
|
||||
private StandaloneCodeEditor? _monacoEditor;
|
||||
private bool _isInternalContentChange;
|
||||
private bool _isInitialized;
|
||||
private string _monacoEditorId = $"monaco-editor-{Guid.NewGuid()}:N";
|
||||
private string? _lastMonacoEditorContent;
|
||||
private RateLimitedFunc<Expression, Task> _throttledValueChanged;
|
||||
private ICollection<ExpressionDescriptor> _expressionDescriptors = new List<ExpressionDescriptor>();
|
||||
private TaskCompletionSource<bool>? _initializationTcs;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExpressionEditor()
|
||||
{
|
||||
_throttledValueChanged = Debouncer.Debounce<Expression, Task>(InvokeValueChangedCallbackAsync, TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The content to render inside the editor.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment ChildContent { get; set; } = null!;
|
||||
|
||||
[Parameter] public Expression? Expression { get; set; } = new(string.Empty, string.Empty);
|
||||
[Parameter] public string DefaultOption { get; set; } = "Default";
|
||||
[Parameter] public string DisplayName { get; set; } = string.Empty;
|
||||
[Parameter] public string Description { get; set; } = string.Empty;
|
||||
[Parameter] public bool ReadOnly { get; set; }
|
||||
[Parameter] public IDictionary<string, object> CustomProperties { get; set; } = new Dictionary<string, object>();
|
||||
[Parameter] public Func<Expression?, Task>? ExpressionChanged { get; set; }
|
||||
|
||||
[Inject] private TypeDefinitionService TypeDefinitionService { get; set; } = null!;
|
||||
[Inject] private IExpressionService ExpressionService { get; set; } = null!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
[Inject] private IEnumerable<IMonacoHandler> MonacoHandlers { get; set; } = null!;
|
||||
[Inject] private IUIHintService UIHintService { get; set; } = null!;
|
||||
private IEnumerable<ExpressionDescriptor> BrowsableExpressionDescriptors => _expressionDescriptors.Where(x => x.IsBrowsable);
|
||||
private ExpressionDescriptor? SelectedExpressionDescriptor => _expressionDescriptors.FirstOrDefault(x => x.Type == _selectedExpressionType);
|
||||
private string SelectedExpressionTypeDisplayName => SelectedExpressionDescriptor?.DisplayName ?? DefaultOption;
|
||||
private string MonacoLanguage => SelectedExpressionDescriptor?.GetMonacoLanguage() ?? string.Empty;
|
||||
private bool DisplayCodeEditor => _selectedExpressionType != null && _selectedExpressionType != DefaultOption;
|
||||
private bool DisplayModePicker => !DisplayCodeEditor;
|
||||
private string? ButtonIcon => DisplayModePicker ? Icons.Material.Filled.MoreVert : null;
|
||||
private string? ButtonLabel => DisplayModePicker ? null : SelectedExpressionTypeDisplayName;
|
||||
private Variant ButtonVariant => DisplayModePicker ? default : Variant.Filled;
|
||||
private Color ButtonColor => DisplayModePicker ? default : Color.Primary;
|
||||
private string? ButtonEndIcon => DisplayModePicker ? null : Icons.Material.Filled.KeyboardArrowDown;
|
||||
private Color ButtonEndColor => DisplayModePicker ? default : Color.Secondary;
|
||||
|
||||
private string? MonacoSyntax { get; set; }
|
||||
private bool MonacoSyntaxExist => !string.IsNullOrEmpty(MonacoLanguage);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the internal state of the editor with the provided expression.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to update the editor with. Can be null.</param>
|
||||
public async Task UpdateAsync(Expression? expression)
|
||||
{
|
||||
if (!_isInitialized || _monacoEditor == null)
|
||||
{
|
||||
_initializationTcs ??= new();
|
||||
await _initializationTcs.Task;
|
||||
}
|
||||
|
||||
await UpdateMonacoEditorAsync(expression);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var expressionDescriptors = (await ExpressionService.ListDescriptorsAsync()).ExceptBy(_expressionTypeBlacklist, x => x.Type).ToList();
|
||||
_selectedExpressionType = expressionDescriptors.First().Type;
|
||||
_expressionDescriptors = expressionDescriptors.ToList();
|
||||
}
|
||||
|
||||
private async Task UpdateMonacoLanguageAsync()
|
||||
{
|
||||
if (_monacoEditor == null)
|
||||
return;
|
||||
|
||||
if (SelectedExpressionDescriptor?.Type == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(MonacoLanguage))
|
||||
return;
|
||||
|
||||
var model = await _monacoEditor.GetModel();
|
||||
await Global.SetModelLanguage(JSRuntime, model, MonacoLanguage);
|
||||
await RunMonacoHandlersAsync(_monacoEditor);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
_selectedExpressionType = Expression?.Type;
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
private StandaloneEditorConstructionOptions ConfigureMonacoEditor(StandaloneCodeEditor editor)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Language = MonacoLanguage,
|
||||
Value = Expression?.Value?.ToString() ?? "",
|
||||
FontFamily = "Roboto Mono, monospace",
|
||||
RenderLineHighlight = "none",
|
||||
FixedOverflowWidgets = true,
|
||||
Minimap = new()
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
AutomaticLayout = true,
|
||||
LineNumbers = "on",
|
||||
Theme = "vs",
|
||||
RoundedSelection = true,
|
||||
ScrollBeyondLastLine = false,
|
||||
OverviewRulerLanes = 0,
|
||||
OverviewRulerBorder = false,
|
||||
LineDecorationsWidth = 0,
|
||||
HideCursorInOverviewRuler = true,
|
||||
GlyphMargin = false,
|
||||
ReadOnly = ReadOnly,
|
||||
DomReadOnly = ReadOnly
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OnSyntaxSelectedAsync(string? syntax)
|
||||
{
|
||||
_selectedExpressionType = syntax;
|
||||
|
||||
var value = Expression?.Value?.ToString() ?? "";
|
||||
var expressionType = syntax ?? DefaultOption;
|
||||
var expression = new Expression(expressionType, value);
|
||||
await InvokeValueChangedCallbackAsync(expression);
|
||||
await UpdateMonacoLanguageAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task OnMonacoContentChangedAsync(ModelContentChangedEvent e)
|
||||
{
|
||||
if (_isInternalContentChange)
|
||||
return;
|
||||
|
||||
var value = await _monacoEditor!.GetValue();
|
||||
|
||||
// This event gets fired even when the content hasn't changed, for example, when the containing pane is resized.
|
||||
// This happens from within the monaco editor itself (or the Blazor wrapper, not sure).
|
||||
if (value == _lastMonacoEditorContent)
|
||||
return;
|
||||
|
||||
var expression = new Expression(_selectedExpressionType!, value);
|
||||
_lastMonacoEditorContent = value;
|
||||
await ThrottleValueChangedCallbackAsync(expression);
|
||||
}
|
||||
|
||||
private async Task ThrottleValueChangedCallbackAsync(Expression expression) => await _throttledValueChanged.InvokeAsync(expression);
|
||||
|
||||
private async Task InvokeValueChangedCallbackAsync(Expression? expression)
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (ExpressionChanged != null)
|
||||
await ExpressionChanged.Invoke(expression);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnMonacoInitializedAsync()
|
||||
{
|
||||
_isInitialized = true;
|
||||
|
||||
if (_initializationTcs != null)
|
||||
{
|
||||
_initializationTcs.SetResult(true);
|
||||
_initializationTcs = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await UpdateMonacoEditorAsync(Expression);
|
||||
}
|
||||
|
||||
private async Task UpdateMonacoEditorAsync(Expression? expression)
|
||||
{
|
||||
_isInternalContentChange = true;
|
||||
var model = await _monacoEditor!.GetModel();
|
||||
var expressionText = expression?.Value?.ToString() ?? string.Empty;
|
||||
|
||||
if (expression?.Type != null)
|
||||
_selectedExpressionType = expression.Type;
|
||||
|
||||
_lastMonacoEditorContent = expressionText;
|
||||
await model.SetValue(expressionText);
|
||||
_isInternalContentChange = false;
|
||||
await Global.SetModelLanguage(JSRuntime, model, MonacoLanguage);
|
||||
await RunMonacoHandlersAsync(_monacoEditor);
|
||||
}
|
||||
|
||||
private async Task RunMonacoHandlersAsync(StandaloneCodeEditor editor)
|
||||
{
|
||||
if (SelectedExpressionDescriptor == null)
|
||||
return;
|
||||
|
||||
var context = new MonacoContext(editor, SelectedExpressionDescriptor, CustomProperties);
|
||||
|
||||
foreach (var handler in MonacoHandlers)
|
||||
await handler.InitializeAsync(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_throttledValueChanged.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@using Elsa.Api.Client.Resources.Scripting.Extensions;
|
||||
@using Elsa.Api.Client.Resources.Scripting.Models;
|
||||
@using Elsa.Studio.Enums
|
||||
|
||||
<div class="d-flex flex-grow-1 gap-4 align-content-start relative">
|
||||
<div class="flex-1">
|
||||
|
||||
@if (ShowMonacoEditor)
|
||||
{
|
||||
<FieldExtension UIHintComponent=@ComponentType EditorContext="@EditorContext">
|
||||
<ChildContent>
|
||||
<MudField Variant="Variant.Outlined" Label="@DisplayName" HelperText="@Description" Margin="Margin.Dense" Disabled="EditorContext.IsReadOnly">
|
||||
<StandaloneCodeEditor @ref="_monacoEditor"
|
||||
Id="@_monacoEditorId"
|
||||
ConstructionOptions="ConfigureMonacoEditor"
|
||||
OnDidInit="OnMonacoInitializedAsync"
|
||||
OnDidChangeModelContent="OnMonacoContentChangedAsync"
|
||||
CssClass="studio-expression-input-monaco-editor" />
|
||||
</MudField>
|
||||
</ChildContent>
|
||||
</FieldExtension>
|
||||
}
|
||||
else
|
||||
{
|
||||
<FieldExtension UIHintComponent=@ComponentType EditorContext="@EditorContext" ChildContent="@ChildContent" />
|
||||
}
|
||||
|
||||
</div>
|
||||
@if (EditorContext.InputDescriptor.IsWrapped)
|
||||
{
|
||||
RenderFragment DisplayContent(ExpressionDescriptor item) => @<div>
|
||||
@{
|
||||
var isChecked = item.Type == _selectedExpressionType;
|
||||
var iconColor = isChecked ? Color.Secondary : Color.Transparent;
|
||||
|
||||
<MudMenuItem Class="studio-expression-input-menu-item" OnClick="@(() => OnSyntaxSelectedAsync(item.Type))" Icon="@Icons.Material.Filled.Check" IconSize="Size.Small" IconColor="iconColor">@item.DisplayName</MudMenuItem>
|
||||
}</div>;
|
||||
<div class="flex-none pt-1">
|
||||
<MudMenu Label="@ButtonLabel" Icon="@ButtonIcon" Variant="@ButtonVariant" Color="@ButtonColor" EndIcon="@ButtonEndIcon" IconColor="@ButtonEndColor" Dense="true" Disabled="EditorContext.IsReadOnly">
|
||||
@DisplayContent(BrowsableExpressionDescriptors.First())
|
||||
<MudDivider />
|
||||
@foreach (var item in BrowsableExpressionDescriptors.Where(i=>!string.IsNullOrEmpty(i.GetMonacoLanguage())))
|
||||
{
|
||||
@DisplayContent(item);
|
||||
}
|
||||
<MudDivider />
|
||||
@foreach (var item in BrowsableExpressionDescriptors.Skip(1).Where(i=>string.IsNullOrEmpty(i.GetMonacoLanguage())))
|
||||
{
|
||||
@DisplayContent(item);
|
||||
}
|
||||
</MudMenu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,244 @@
|
||||
using BlazorMonaco.Editor;
|
||||
using Elsa.Api.Client.Resources.ActivityDescriptors.Models;
|
||||
using Elsa.Api.Client.Resources.Scripting.Extensions;
|
||||
using Elsa.Api.Client.Resources.Scripting.Models;
|
||||
using Elsa.Api.Client.Shared.Models;
|
||||
using Elsa.Studio.Contracts;
|
||||
using Elsa.Studio.Extensions;
|
||||
using Elsa.Studio.Models;
|
||||
using Elsa.Studio.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using ThrottleDebounce;
|
||||
|
||||
namespace Elsa.Studio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A component that renders an input for an expression.
|
||||
/// </summary>
|
||||
public partial class ExpressionInput : IDisposable
|
||||
{
|
||||
private const string DefaultSyntax = "Literal";
|
||||
private readonly string[] _uiSyntaxes = ["Literal", "Object"];
|
||||
private string _selectedExpressionType = DefaultSyntax;
|
||||
private string _selectedExpressionTypeDisplayName = DefaultSyntax;
|
||||
private StandaloneCodeEditor? _monacoEditor = null!;
|
||||
private bool _isInternalContentChange;
|
||||
private string _monacoEditorId = $"monaco-editor-{Guid.NewGuid()}:N";
|
||||
private string? _lastMonacoEditorContent;
|
||||
private RateLimitedFunc<WrappedInput, Task> _throttledValueChanged;
|
||||
private ICollection<ExpressionDescriptor> _expressionDescriptors = new List<ExpressionDescriptor>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExpressionInput()
|
||||
{
|
||||
_throttledValueChanged = Debouncer.Debounce<WrappedInput, Task>(InvokeValueChangedCallbackAsync, TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The context for the editor.
|
||||
/// </summary>
|
||||
[Parameter] public DisplayInputEditorContext EditorContext { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The content to render inside the editor.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment ChildContent { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A flag indicating whether the editor should only display code.
|
||||
/// </summary>
|
||||
[Parameter] public bool IsCodeOnly { get; set; }
|
||||
|
||||
[Inject] private TypeDefinitionService TypeDefinitionService { get; set; } = null!;
|
||||
[Inject] private IExpressionService ExpressionService { get; set; } = null!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
[Inject] private IEnumerable<IMonacoHandler> MonacoHandlers { get; set; } = null!;
|
||||
[Inject] private IUIHintService UIHintService { get; set; } = null!;
|
||||
private IEnumerable<ExpressionDescriptor> BrowsableExpressionDescriptors => _expressionDescriptors.Where(x => x.IsBrowsable);
|
||||
private ExpressionDescriptor? SelectedExpressionDescriptor => _expressionDescriptors.FirstOrDefault(x => x.Type == _selectedExpressionType);
|
||||
private string MonacoLanguage => SelectedExpressionDescriptor?.GetMonacoLanguage() ?? string.Empty;
|
||||
private string UISyntax => EditorContext.UIHintHandler.UISyntax;
|
||||
private string ComponentType => EditorContext.InputDescriptor.UIHint;
|
||||
private bool IsUISyntax => !IsCodeOnly && _selectedExpressionType == UISyntax;
|
||||
private string? ButtonIcon => IsUISyntax ? Icons.Material.Filled.MoreVert : null;
|
||||
private string? ButtonLabel => IsUISyntax ? null : _selectedExpressionTypeDisplayName;
|
||||
private Variant ButtonVariant => IsUISyntax ? default : Variant.Filled;
|
||||
private Color ButtonColor => IsUISyntax ? default : Color.Primary;
|
||||
private string? ButtonEndIcon => IsUISyntax ? null : Icons.Material.Filled.KeyboardArrowDown;
|
||||
private Color ButtonEndColor => IsUISyntax ? default : Color.Secondary;
|
||||
private bool ShowMonacoEditor => IsCodeOnly || (!IsUISyntax && EditorContext.InputDescriptor.IsWrapped && MonacoSyntaxExist);
|
||||
private string DisplayName => EditorContext.InputDescriptor.DisplayName ?? EditorContext.InputDescriptor.Name;
|
||||
private string? Description => EditorContext.InputDescriptor.Description;
|
||||
private string InputValue => EditorContext.GetExpressionValueOrDefault();
|
||||
|
||||
private string? MonacoSyntax { get; set; }
|
||||
private bool MonacoSyntaxExist => !string.IsNullOrEmpty(MonacoLanguage);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var defaultDescriptor = new ExpressionDescriptor(UISyntax, "Default");
|
||||
var expressionDescriptors = (await ExpressionService.ListDescriptorsAsync()).ExceptBy(_uiSyntaxes, x => x.Type).ToList();
|
||||
|
||||
if (IsCodeOnly)
|
||||
_selectedExpressionType = expressionDescriptors.First().Type;
|
||||
else
|
||||
expressionDescriptors = expressionDescriptors.Prepend(defaultDescriptor).ToList();
|
||||
|
||||
_expressionDescriptors = expressionDescriptors.ToList();
|
||||
}
|
||||
|
||||
private async Task UpdateMonacoLanguageAsync()
|
||||
{
|
||||
if (_monacoEditor == null)
|
||||
return;
|
||||
|
||||
var expressionDescriptor = SelectedExpressionDescriptor;
|
||||
|
||||
if (expressionDescriptor == null)
|
||||
return;
|
||||
|
||||
var monacoLanguage = expressionDescriptor.GetMonacoLanguage();
|
||||
_selectedExpressionTypeDisplayName = expressionDescriptor.DisplayName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(monacoLanguage))
|
||||
return;
|
||||
|
||||
var model = await _monacoEditor.GetModel();
|
||||
await Global.SetModelLanguage(JSRuntime, model, monacoLanguage);
|
||||
await RunMonacoHandlersAsync(_monacoEditor);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
var selectedExpressionDescriptor = EditorContext.SelectedExpressionDescriptor;
|
||||
_selectedExpressionType = selectedExpressionDescriptor?.Type ?? UISyntax;
|
||||
_selectedExpressionTypeDisplayName = selectedExpressionDescriptor?.DisplayName ?? UISyntax;
|
||||
|
||||
UpdateUIHintAsync(EditorContext);
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
private StandaloneEditorConstructionOptions ConfigureMonacoEditor(StandaloneCodeEditor editor)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Language = MonacoLanguage,
|
||||
Value = InputValue,
|
||||
FontFamily = "Roboto Mono, monospace",
|
||||
RenderLineHighlight = "none",
|
||||
FixedOverflowWidgets = true,
|
||||
Minimap = new()
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
AutomaticLayout = true,
|
||||
LineNumbers = "on",
|
||||
Theme = "vs",
|
||||
RoundedSelection = true,
|
||||
ScrollBeyondLastLine = false,
|
||||
OverviewRulerLanes = 0,
|
||||
OverviewRulerBorder = false,
|
||||
LineDecorationsWidth = 0,
|
||||
HideCursorInOverviewRuler = true,
|
||||
GlyphMargin = false,
|
||||
ReadOnly = EditorContext.IsReadOnly,
|
||||
DomReadOnly = EditorContext.IsReadOnly
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OnMonacoInitializedAsync()
|
||||
{
|
||||
_isInternalContentChange = true;
|
||||
var model = await _monacoEditor!.GetModel();
|
||||
_lastMonacoEditorContent = InputValue;
|
||||
await model.SetValue(InputValue);
|
||||
_isInternalContentChange = false;
|
||||
await Global.SetModelLanguage(JSRuntime, model, MonacoLanguage);
|
||||
await RunMonacoHandlersAsync(_monacoEditor);
|
||||
}
|
||||
|
||||
private async Task OnSyntaxSelectedAsync(string syntax)
|
||||
{
|
||||
_selectedExpressionType = syntax;
|
||||
|
||||
var value = InputValue;
|
||||
var input = (WrappedInput?)EditorContext.Value ?? new WrappedInput();
|
||||
|
||||
input.Expression = new(_selectedExpressionType, value);
|
||||
await InvokeValueChangedCallbackAsync(input);
|
||||
await UpdateMonacoLanguageAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to display the UI hint when the user selects any other choice than a language supported by the monaco editor.
|
||||
/// </summary>
|
||||
private void UpdateUIHintAsync(DisplayInputEditorContext editorContext)
|
||||
{
|
||||
if (_selectedExpressionType == "Variable")
|
||||
{
|
||||
var uiHintHandler = UIHintService.GetHandler("variable-picker");
|
||||
var editor = uiHintHandler.DisplayInputEditor(editorContext);
|
||||
ChildContent = editor;
|
||||
return;
|
||||
}
|
||||
|
||||
if (MonacoSyntax == null
|
||||
&& _selectedExpressionType == editorContext.SelectedExpressionDescriptor?.Type
|
||||
&& editorContext.SelectedExpressionDescriptor.Properties.TryGetValue("UIHint", out var uiHint))
|
||||
{
|
||||
var uiHintHandler = UIHintService.GetHandler(uiHint);
|
||||
var editor = uiHintHandler.DisplayInputEditor(editorContext);
|
||||
_selectedExpressionTypeDisplayName = editorContext.SelectedExpressionDescriptor.DisplayName;
|
||||
|
||||
ChildContent = editor;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnMonacoContentChangedAsync(ModelContentChangedEvent e)
|
||||
{
|
||||
if (_isInternalContentChange)
|
||||
return;
|
||||
|
||||
var value = await _monacoEditor!.GetValue();
|
||||
|
||||
// This event gets fired even when the content hasn't changed, but for example when the containing pane is resized.
|
||||
// This happens from within the monaco editor itself (or the Blazor wrapper, not sure).
|
||||
if (value == _lastMonacoEditorContent)
|
||||
return;
|
||||
|
||||
var input = (WrappedInput?)EditorContext.Value ?? new WrappedInput();
|
||||
input.Expression = new(_selectedExpressionType, value);
|
||||
_lastMonacoEditorContent = value;
|
||||
await ThrottleValueChangedCallbackAsync(input);
|
||||
}
|
||||
|
||||
private async Task ThrottleValueChangedCallbackAsync(WrappedInput input) => await _throttledValueChanged.InvokeAsync(input);
|
||||
|
||||
private async Task InvokeValueChangedCallbackAsync(WrappedInput input)
|
||||
{
|
||||
await InvokeAsync(async () => await EditorContext.OnValueChanged(input));
|
||||
}
|
||||
|
||||
private async Task RunMonacoHandlersAsync(StandaloneCodeEditor editor)
|
||||
{
|
||||
var customProps = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(ActivityDescriptor), EditorContext.ActivityDescriptor },
|
||||
{ nameof(PropertyDescriptor), EditorContext.InputDescriptor },
|
||||
{ "WorkflowDefinitionId", EditorContext.WorkflowDefinition.DefinitionId }
|
||||
};
|
||||
|
||||
var expressionDescriptor = SelectedExpressionDescriptor?? new ExpressionDescriptor("Literal", "Default");
|
||||
var context = new MonacoContext(editor, expressionDescriptor, customProps);
|
||||
|
||||
foreach (var handler in MonacoHandlers)
|
||||
await handler.InitializeAsync(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose() => _throttledValueChanged.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
@using Elsa.Studio.Contracts
|
||||
@using Elsa.Studio.Enums
|
||||
@using Elsa.Studio.Models
|
||||
|
||||
<style>
|
||||
.field-container {
|
||||
transition: border 0.3s ease-in-out;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: -10px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.field-container.hovered {
|
||||
border-color: #ccc
|
||||
}
|
||||
|
||||
.no-padding-stack {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (hasExtensions)
|
||||
{
|
||||
<MudStack Spacing="1" Class=@($"field-container {(isHovered && hasExtensions ? "hovered" : "")}") @onmouseenter="() => isHovered = true" @onmouseleave="() => isHovered = false">
|
||||
@foreach (var extension in TopExtensions ?? Enumerable.Empty<IUIFieldExtensionHandler>())
|
||||
{
|
||||
@extension.DisplayExtension(EditorContext)
|
||||
}
|
||||
@ChildContent
|
||||
@foreach (var extension in BottomExtensions ?? Enumerable.Empty<IUIFieldExtensionHandler>())
|
||||
{
|
||||
@extension.DisplayExtension(EditorContext)
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
|
||||
|
||||
@code
|
||||
{
|
||||
private bool isHovered = false;
|
||||
private bool hasExtensions => AllExtensions?.Count != 0;
|
||||
private List<IUIFieldExtensionHandler>? AllExtensions;
|
||||
private List<IUIFieldExtensionHandler>? TopExtensions;
|
||||
private List<IUIFieldExtensionHandler>? BottomExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// The UI Hint component type the extensions should be rendered for.
|
||||
/// </summary>
|
||||
[Parameter] public string UIHintComponent { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The editor context.
|
||||
/// </summary>
|
||||
[Parameter] public DisplayInputEditorContext EditorContext { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The content wrap with <see cref="IUIFieldExtensionHandler"/> extensions.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
|
||||
|
||||
[Inject] private IUIFieldExtensionService FieldExtensionService { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
AllExtensions = FieldExtensionService.TryGetHandlers(UIHintComponent, EditorContext);
|
||||
if (AllExtensions is not null)
|
||||
{
|
||||
TopExtensions = AllExtensions.Where(p => p.Position == FieldExtensionPosition.Top).ToList();
|
||||
BottomExtensions = AllExtensions.Where(p => p.Position == FieldExtensionPosition.Bottom).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@using Elsa.Studio.Contracts
|
||||
@using Elsa.Studio.Models
|
||||
@inject IMenuService MenuService;
|
||||
|
||||
<MudNavMenu Bordered="true" Color="Color.Primary" Rounded="true" Margin="Margin.Dense" Class="pa-2">
|
||||
@foreach (var menuItemGroup in _menuItemsGroups.Values)
|
||||
{
|
||||
var menuItems = _menuItems.Where(x => x.GroupName == menuItemGroup.Name).ToList();
|
||||
|
||||
@if (menuItems.Any())
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="px-4 py-4 mud-text-secondary">@menuItemGroup.Text</MudText>
|
||||
|
||||
foreach (var menuItem in menuItems)
|
||||
{
|
||||
<NavMenuItem MenuItem="menuItem" />
|
||||
}
|
||||
}
|
||||
}
|
||||
</MudNavMenu>
|
||||
|
||||
@code
|
||||
{
|
||||
private IDictionary<string, MenuItemGroup> _menuItemsGroups = new Dictionary<string, MenuItemGroup>();
|
||||
private IEnumerable<MenuItem> _menuItems = new List<MenuItem>();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_menuItemsGroups = (await MenuService.GetMenuItemGroupsAsync()).ToDictionary(x => x.Name);
|
||||
_menuItems = await MenuService.GetMenuItemsAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@using Elsa.Studio.Models
|
||||
@if (!MenuItem.SubMenuItems.Any())
|
||||
{
|
||||
<MudNavLink Href="@MenuItem.Href" Match="@MenuItem.Match" Icon="@MenuItem.Icon">@MenuItem.Text</MudNavLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudNavGroup Icon="@MenuItem.Icon" Title="@MenuItem.Text">
|
||||
@foreach (var subItem in MenuItem.SubMenuItems)
|
||||
{
|
||||
<NavMenuItem MenuItem="@subItem"/>
|
||||
}
|
||||
</MudNavGroup>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public MenuItem MenuItem { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary" Class="my-6">@Text</MudText>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Text { get; set; } = "Untitled";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="0" AlignItems="AlignItems.Start" Row="false">
|
||||
<ClientPackageVersion/>
|
||||
<ServerPackageVersion/>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" OnClick="@Close">Close</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
void Close() => MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<div style="@($"max-height: {MaxHeight}px; overflow-y: scroll;")">
|
||||
<div class="pa-4">
|
||||
@ChildContent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum height of the <see cref="ScrollableWell"/> component.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int MaxHeight { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// The content to be rendered inside the <see cref="ScrollableWell"/> component.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@using Elsa.Studio.Contracts
|
||||
@inherits StudioComponentBase
|
||||
<MudText Typo="Typo.body2">Server version: @PackageVersion</MudText>
|
||||
|
||||
@code {
|
||||
[Inject] private IServerInformationProvider ServerInformationProvider { get; set; } = default!;
|
||||
private string PackageVersion { get; set; } = "Loading...";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var info = await ServerInformationProvider.GetInfoAsync();
|
||||
PackageVersion = info.PackageVersion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Elsa.Studio.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Elsa.Studio.Components;
|
||||
|
||||
/// Base class for components. This class sets the <see cref="BlazorServiceAccessor.Services"/> property to the <see cref="IServiceProvider"/> instance.
|
||||
/// See https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0#access-server-side-blazor-services-from-a-different-di-scope
|
||||
public abstract class StudioComponentBase : ComponentBase;
|
||||
@@ -0,0 +1 @@
|
||||
<MudAlert Severity="Severity.Normal" Icon="@Icons.Material.Outlined.Announcement">Unauthorized</MudAlert>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="pa-2" style="@($"overflow-y: scroll; max-height: calc(100vh - var(--mud-appbar-height) - {ExtraPadding}px);")">
|
||||
@ChildContent
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
/// <summary>
|
||||
/// The content to be rendered inside the <see cref="VerticalWell"/> component.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of extra padding to add to the bottom of the <see cref="VerticalWell"/> component.
|
||||
/// </summary>
|
||||
[Parameter] public int ExtraPadding { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="pa-4">
|
||||
@ChildContent
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
/// <summary>
|
||||
/// The content to be rendered inside the <see cref="Well"/> component.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@foreach (var widget in Widgets)
|
||||
{
|
||||
@widget.Render(Attributes)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Elsa.Studio.Contracts;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Elsa.Studio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a zone in which widgets can be rendered.
|
||||
/// </summary>
|
||||
public partial class Zone : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the zone name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the attributes that are passed to the widgets.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IDictionary<string, object?> Attributes { get; set; } = new Dictionary<string, object?>();
|
||||
|
||||
[Inject] private IWidgetRegistry WidgetRegistry { get; set; } = default!;
|
||||
[Inject] private IFeatureService FeatureService { get; set; } = default!;
|
||||
private ICollection<IWidget> Widgets { get; set; } = new List<IWidget>();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
FeatureService.Initialized += OnFeatureServiceInitialized;
|
||||
Widgets = WidgetRegistry.List(Name).ToList();
|
||||
}
|
||||
|
||||
private void OnFeatureServiceInitialized()
|
||||
{
|
||||
Widgets = WidgetRegistry.List(Name).ToList();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
FeatureService.Initialized -= OnFeatureServiceInitialized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Studio.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts primitives to and from JSON strings.
|
||||
/// </summary>
|
||||
public class JsonPrimitiveToStringConverter : JsonConverter<string>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.True:
|
||||
return "True";
|
||||
case JsonTokenType.False:
|
||||
return "False";
|
||||
case JsonTokenType.Number when reader.TryGetInt64(out var l):
|
||||
return l.ToString();
|
||||
case JsonTokenType.Number:
|
||||
return reader.GetDouble().ToString(CultureInfo.InvariantCulture);
|
||||
case JsonTokenType.String when reader.TryGetDateTimeOffset(out var datetime):
|
||||
return datetime.ToString();
|
||||
case JsonTokenType.String:
|
||||
return reader.GetString()!;
|
||||
default:
|
||||
{
|
||||
// Use JsonElement as fallback.
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
return document.RootElement.Clone().ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Studio.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts strings to and from integers.
|
||||
/// </summary>
|
||||
public class JsonStringToIntConverter : JsonConverter<int>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.String:
|
||||
{
|
||||
var stringValue = reader.GetString();
|
||||
if (int.TryParse(stringValue, out var intValue))
|
||||
return intValue;
|
||||
break;
|
||||
}
|
||||
case JsonTokenType.Number:
|
||||
return reader.GetInt32();
|
||||
}
|
||||
|
||||
throw new JsonException($"Unable to convert {reader.TokenType} to {nameof(Int32)}.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Elsa Studio shared services and components.</Description>
|
||||
<PackageTags>elsa studio framework</PackageTags>
|
||||
<RootNamespace>Elsa.Studio</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" />
|
||||
<PackageReference Include="BlazorMonaco" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Studio.Core\Elsa.Studio.Core.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Studio.DomInterop\Elsa.Studio.DomInterop.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,3 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=localization_005Ctime_005Ccontracts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=localization_005Ctime_005Cimplementations/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Elsa.Studio.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IEnumerable{Claim}" />.
|
||||
/// </summary>
|
||||
public static class ClaimsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the "exp" claim is expired.
|
||||
/// </summary>
|
||||
public static bool IsExpired(this IEnumerable<Claim> claims)
|
||||
{
|
||||
var expString = claims.FirstOrDefault(x => x.Type == "exp")?.Value.Trim();
|
||||
var exp = !string.IsNullOrEmpty(expString) ? long.Parse(expString) : 0;
|
||||
var expiresAt = DateTimeOffset.FromUnixTimeSeconds(exp);
|
||||
return expiresAt < DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Elsa.Studio.Branding;
|
||||
using Elsa.Studio.Contracts;
|
||||
using Elsa.Studio.Localization.Time;
|
||||
using Elsa.Studio.Localization.Time.Providers;
|
||||
using Elsa.Studio.Monaco.Handlers;
|
||||
using Elsa.Studio.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Studio.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Adds shared services to the service collection.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds shared services to the service collection.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSharedServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<TypeDefinitionService>();
|
||||
|
||||
// TODO: Move this to a new package; either specific to JS, or perhaps a general Monaco package that supports multiple languages.
|
||||
// We'll decide once we add support for more languages.
|
||||
services.AddScoped<IMonacoHandler, JavaScriptMonacoHandler>();
|
||||
|
||||
// Time services.
|
||||
services.AddScoped<ITimeFormatter, DefaultTimeFormatter>();
|
||||
services.AddScoped<ITimeZoneProvider, UtcTimeZoneProvider>();
|
||||
services.AddScoped<IBrandingProvider, DefaultBrandingProvider>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
@@ -0,0 +1,30 @@
|
||||
@using Elsa.Studio.Contracts
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject IThemeService ThemeService
|
||||
|
||||
<MudThemeProvider Theme="CurrentTheme" IsDarkMode="@true"/>
|
||||
<MudSnackbarProvider/>
|
||||
<MudPopoverProvider/>
|
||||
|
||||
<MudLayout>
|
||||
@Body
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
|
||||
private MudTheme CurrentTheme => ThemeService.CurrentTheme;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ThemeService.CurrentThemeChanged += OnThemeChanged;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ThemeService.CurrentThemeChanged -= OnThemeChanged;
|
||||
}
|
||||
|
||||
private void OnThemeChanged() => StateHasChanged();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider IsDarkMode="@IsDarkMode" Theme="CurrentTheme"/>
|
||||
<MudPopoverProvider/>
|
||||
<MudDialogProvider/>
|
||||
<MudSnackbarProvider/>
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="0" Color="Color.Inherit">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@DrawerToggle"/>
|
||||
<MudSpacer/>
|
||||
@foreach (var appBarItem in AppBarService.AppBarItems)
|
||||
{
|
||||
@appBarItem
|
||||
}
|
||||
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Book" Color="Color.Inherit" Href="https://v3.elsaworkflows.io/" Target="_blank" Title="Documentation"/>
|
||||
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/elsa-workflows/elsa-core" Target="_blank" Title="Source code"/>
|
||||
|
||||
@if (IsDarkMode)
|
||||
{
|
||||
<MudToggleIconButton Icon="@Icons.Material.Outlined.LightMode" Color="Color.Inherit" @onclick="@ToggleDarkMode"></MudToggleIconButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudToggleIconButton Icon="@Icons.Material.Outlined.DarkMode" Color="Color.Inherit" @onclick="@ToggleDarkMode"></MudToggleIconButton>
|
||||
}
|
||||
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" @onclick="@ShowProductInfo"/>
|
||||
</MudAppBar>
|
||||
<MudDrawer @bind-Open="_drawerOpen" Elevation="0" ClipMode="DrawerClipMode.Always">
|
||||
<MudDrawerHeader>
|
||||
@BrandingProvider.Branding
|
||||
</MudDrawerHeader>
|
||||
<div class="mt-10">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
<ErrorBoundary @ref="@_errorBoundary">
|
||||
<ChildContent>
|
||||
@Body
|
||||
</ChildContent>
|
||||
<ErrorContent>
|
||||
@if (context is UnauthorizedAccessException)
|
||||
{
|
||||
@UnauthorizedComponent
|
||||
}
|
||||
else
|
||||
{
|
||||
<Well>
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
|
||||
@context.GetType().Name: @context.Message
|
||||
</MudAlert>
|
||||
</Well>
|
||||
}
|
||||
</ErrorContent>
|
||||
</ErrorBoundary>
|
||||
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
@@ -0,0 +1,94 @@
|
||||
using Blazored.LocalStorage;
|
||||
using Elsa.Studio.Branding;
|
||||
using Elsa.Studio.Components;
|
||||
using Elsa.Studio.Contracts;
|
||||
using Elsa.Studio.Extensions;
|
||||
using Elsa.Studio.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Elsa.Studio.Layouts;
|
||||
|
||||
/// <summary>
|
||||
/// The main layout for the application.
|
||||
/// </summary>
|
||||
public partial class MainLayout : IDisposable
|
||||
{
|
||||
private bool _drawerOpen = true;
|
||||
private ErrorBoundary? _errorBoundary;
|
||||
|
||||
[Inject] private IThemeService ThemeService { get; set; } = null!;
|
||||
[Inject] private IAppBarService AppBarService { get; set; } = null!;
|
||||
[Inject] private IUnauthorizedComponentProvider UnauthorizedComponentProvider { get; set; } = null!;
|
||||
[Inject] private IFeatureService FeatureService { get; set; } = null!;
|
||||
[Inject] private IDialogService DialogService { get; set; } = null!;
|
||||
[Inject] private IBrandingProvider BrandingProvider { get; set; } = null!;
|
||||
[Inject] private IServiceProvider ServiceProvider { get; set; } = null!;
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthenticationState { get; set; }
|
||||
private MudTheme CurrentTheme => ThemeService.CurrentTheme;
|
||||
private bool IsDarkMode => ThemeService.IsDarkMode;
|
||||
private RenderFragment UnauthorizedComponent => UnauthorizedComponentProvider.GetUnauthorizedComponent();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ThemeService.CurrentThemeChanged += OnThemeChanged;
|
||||
AppBarService.AppBarItemsChanged += OnAppBarItemsChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthenticationState != null)
|
||||
{
|
||||
var authState = await AuthenticationState;
|
||||
if (authState.User.Identity?.IsAuthenticated == true && !authState.User.Claims.IsExpired())
|
||||
{
|
||||
await FeatureService.InitializeFeaturesAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await FeatureService.InitializeFeaturesAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_errorBoundary?.Recover();
|
||||
}
|
||||
|
||||
private void OnThemeChanged() => StateHasChanged();
|
||||
private void OnAppBarItemsChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
private void DrawerToggle()
|
||||
{
|
||||
_drawerOpen = !_drawerOpen;
|
||||
}
|
||||
|
||||
private void ToggleDarkMode()
|
||||
{
|
||||
ThemeService.IsDarkMode = !ThemeService.IsDarkMode;
|
||||
}
|
||||
|
||||
private async Task ShowProductInfo()
|
||||
{
|
||||
await DialogService.ShowAsync<ProductInfoDialog>($"Elsa Studio {ToolVersion.GetDisplayVersion()}", new DialogOptions
|
||||
{
|
||||
FullWidth = true,
|
||||
MaxWidth = MaxWidth.ExtraSmall,
|
||||
CloseButton = true,
|
||||
CloseOnEscapeKey = true
|
||||
});
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
ThemeService.CurrentThemeChanged -= OnThemeChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@GetDisplayString()
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Elsa.Studio.Localization.Time.Components;
|
||||
|
||||
public partial class Timestamp : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp to display.
|
||||
/// </summary>
|
||||
[Parameter] public DateTimeOffset? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format to use when displaying the timestamp.
|
||||
/// </summary>
|
||||
[Parameter] public string Format { get; set; } = "G";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the string to display when <see cref="Value"/> is <c>null</c>.
|
||||
/// </summary>
|
||||
[Parameter] public string EmptyString { get; set; } = string.Empty;
|
||||
|
||||
[Inject] private ITimeFormatter TimeFormatter { get; set; } = default!;
|
||||
|
||||
private string GetDisplayString()
|
||||
{
|
||||
return Value == null ? EmptyString : TimeFormatter.Format(Value.Value, Format);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Elsa.Studio.Localization.Time;
|
||||
|
||||
/// <summary>
|
||||
/// Formats a date and time value based on the preferred time zone provided by <see cref="ITimeZoneProvider"/>.
|
||||
/// </summary>
|
||||
public interface ITimeFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Formats the specified date and time value.
|
||||
/// </summary>
|
||||
/// <param name="value">The date and time value to format.</param>
|
||||
/// <param name="format">The format to use. Defaults to "G".</param>
|
||||
/// <param name="emptyString">The string to return when <paramref name="value"/> is <c>null</c>.</param>
|
||||
string Format(DateTimeOffset? value, string format = "G", string emptyString = "");
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Elsa.Studio.Localization.Time;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the preferred time zone.
|
||||
/// </summary>
|
||||
public interface ITimeZoneProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the preferred time zone.
|
||||
/// </summary>
|
||||
TimeZoneInfo GetTimeZone();
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Elsa.Studio.Localization.Time;
|
||||
|
||||
/// <summary>
|
||||
/// A default implementation that leverages the currently installed <see cref="ITimeZoneProvider"/>.
|
||||
/// </summary>
|
||||
public class DefaultTimeFormatter(ITimeZoneProvider timeZoneProvider) : ITimeFormatter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Format(DateTimeOffset? value, string format, string emptyString)
|
||||
{
|
||||
if (value == null)
|
||||
return emptyString;
|
||||
|
||||
var timeZone = timeZoneProvider.GetTimeZone();
|
||||
var localTime = TimeZoneInfo.ConvertTime(value.Value, timeZone);
|
||||
return localTime.ToString(format, CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Elsa.Studio.Localization.Time.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the preferred time zone based on the server's local time zone.
|
||||
/// </summary>
|
||||
public class LocalTimeZoneProvider : ITimeZoneProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TimeZoneInfo GetTimeZone()
|
||||
{
|
||||
var timeZoneId = TimeZoneInfo.Local.Id;
|
||||
return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Elsa.Studio.Localization.Time.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the UTC time zone.
|
||||
/// </summary>
|
||||
public class UtcTimeZoneProvider : ITimeZoneProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TimeZoneInfo GetTimeZone()
|
||||
{
|
||||
return TimeZoneInfo.Utc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Elsa.Studio.Models;
|
||||
|
||||
public record DataPanelItem(string Label, string? Text = null, string? Link = null, Func<Task>? OnClick = null);
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Elsa.Studio.Models;
|
||||
|
||||
public class DataPanelModel : List<DataPanelItem>
|
||||
{
|
||||
public DataPanelModel()
|
||||
{
|
||||
}
|
||||
|
||||
public DataPanelModel(IEnumerable<DataPanelItem> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public void Add(string label, string? text = null, string? link = null) => Add(new DataPanelItem(label, text, link));
|
||||
}
|
||||
|
||||
public static class DataPanelModelLinqExtensions
|
||||
{
|
||||
public static DataPanelModel ToDataPanelModel(this IEnumerable<DataPanelItem> items) => new(items);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Elsa.Studio.Models;
|
||||
|
||||
namespace Elsa.Studio.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Allows extensions to customize Monaco editor behavior, such as configuring intellisense.
|
||||
/// </summary>
|
||||
public interface IMonacoHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the Monaco editor is initialized.
|
||||
/// </summary>
|
||||
ValueTask InitializeAsync(MonacoContext context);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Elsa.Api.Client.Resources.ActivityDescriptors.Models;
|
||||
using Elsa.Studio.Contracts;
|
||||
using Elsa.Studio.Models;
|
||||
using Elsa.Studio.Services;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Elsa.Studio.Monaco.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handles Monaco editor for JavaScript.
|
||||
/// </summary>
|
||||
public class JavaScriptMonacoHandler(IJSRuntime jsRuntime, TypeDefinitionService typeDefinitionService)
|
||||
: IMonacoHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeAsync(MonacoContext context)
|
||||
{
|
||||
if (context.ExpressionDescriptor.Type != "JavaScript")
|
||||
return;
|
||||
|
||||
var activityDescriptor = context.CustomProperties.TryGetValue(nameof(ActivityDescriptor), out var activityDescriptorObj) ? (ActivityDescriptor)activityDescriptorObj : null;
|
||||
var propertyDescriptor = context.CustomProperties.TryGetValue(nameof(PropertyDescriptor), out var propertyDescriptorObj) ? (PropertyDescriptor)propertyDescriptorObj : null;
|
||||
var workflowDefinitionId = context.CustomProperties.TryGetValue("WorkflowDefinitionId", out var workflowDefinitionIdObj) ? (string)workflowDefinitionIdObj : null;
|
||||
var activityTypeName = activityDescriptor?.TypeName;
|
||||
var propertyName = propertyDescriptor?.Name;
|
||||
|
||||
if (activityTypeName == null || workflowDefinitionId == null)
|
||||
return;
|
||||
|
||||
var data = await typeDefinitionService.GetTypeDefinition(workflowDefinitionId, activityTypeName, propertyName);
|
||||
await jsRuntime.InvokeVoidAsync("monaco.languages.typescript.javascriptDefaults.addExtraLib", data, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using BlazorMonaco.Editor;
|
||||
using Elsa.Api.Client.Resources.Scripting.Models;
|
||||
|
||||
namespace Elsa.Studio.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a context for working with the Monaco editor.
|
||||
/// </summary>
|
||||
public record MonacoContext(StandaloneCodeEditor Editor, ExpressionDescriptor ExpressionDescriptor, IDictionary<string, object> CustomProperties);
|
||||
@@ -0,0 +1,20 @@
|
||||
using Elsa.Api.Client.Resources.Scripting.Contracts;
|
||||
using Elsa.Studio.Contracts;
|
||||
|
||||
namespace Elsa.Studio.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A service that provides TypeScript type definitions.
|
||||
/// </summary>
|
||||
public class TypeDefinitionService(IBackendApiClientProvider remoteBackendApiClientProvider)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type definition for the specified activity type.
|
||||
/// </summary>
|
||||
public async Task<string> GetTypeDefinition(string definitionId, string activityTypeName, string propertyName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var api = await remoteBackendApiClientProvider.GetApiAsync<IJavaScriptApi>(cancellationToken);
|
||||
var data = await api.GetTypeDefinitions(definitionId, new(definitionId, activityTypeName, propertyName), cancellationToken);
|
||||
return await data.Content.ReadAsStringAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Elsa.Studio;
|
||||
|
||||
public class WellKnownSyntaxNames
|
||||
{
|
||||
public const string Literal = "Literal";
|
||||
public const string Object = "Object";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using BlazorMonaco
|
||||
@using BlazorMonaco.Editor
|
||||
@using MudBlazor
|
||||
@using Elsa.Studio.Components
|
||||
@using Elsa.Studio.Services
|
||||
Reference in New Issue
Block a user