Add project files.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user