Add project files.

This commit is contained in:
2025-08-20 11:07:36 +03:30
parent 3863c74931
commit 1a308a4069
899 changed files with 85379 additions and 0 deletions
@@ -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();
}
@@ -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