@if (!string.IsNullOrEmpty(_searchValue))
{
@@ -196,37 +203,33 @@
}
else
{
- @foreach (var item in _searchResult)
+ @foreach (var item in GetPagedSearchResult())
{
@if (!CurrentView.IsWatchTable || item is ITwinPrimitive)
{
-
- @if (item is ITwinObject)
- {
-
- }
- else if (item is ITwinPrimitive)
- {
-
- }
+
}
}
+ @if (_searchResultTotalCount > 0)
+ {
+
+ }
}
diff --git a/src/base/src/AXOpen.VisualComposer/Components/VisualComposerContainer.razor.cs b/src/base/src/AXOpen.VisualComposer/Components/VisualComposerContainer.razor.cs
index e09290f76..f5c923b65 100644
--- a/src/base/src/AXOpen.VisualComposer/Components/VisualComposerContainer.razor.cs
+++ b/src/base/src/AXOpen.VisualComposer/Components/VisualComposerContainer.razor.cs
@@ -9,6 +9,7 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using Operon.Components;
+using Operon.Components.Toast;
using System.Text.RegularExpressions;
namespace AXOpen.VisualComposer.Components
@@ -27,6 +28,9 @@ public partial class VisualComposerContainer
[Inject]
private ProtectedLocalStorage _protectedLocalStorage { set; get; }
+ [Inject]
+ private IToastService _toastService { get; set; }
+
//private bool _editSVG { get; set; } = false;
private bool _inDesignMode { get; set; } = false;
private Guid _backgroundId { get; set; } = Guid.NewGuid();
@@ -57,6 +61,8 @@ public partial class VisualComposerContainer
private double _optionsMoveRight { get; set; } = 15;
private bool _customPresentation { get; set; } = false;
+ private const string OptionsStorageKey = "VisualComposer_ControllerObjectsOptions";
+
// Watch table filtering and sorting
private string? _watchTableFilter { get; set; } = null;
private bool? _watchTableSortAscending { get; set; } = null;
@@ -213,7 +219,16 @@ private async Task MoveObjectDown(VisualComposerItemData item)
private async Task CreateNewViewAsync(string name, SaveLocationType saveLocationType, bool isWatchTable)
{
if (string.IsNullOrEmpty(name))
+ {
+ _toastService?.AddToast(eToastType.Warning, "View not created", "Please enter a view name.", 5);
+ return;
+ }
+
+ if (_serverStorageAllViews.Contains(name) || _localStorageData.ContainsKey(name))
+ {
+ _toastService?.AddToast(eToastType.Warning, "View not created", $"A view with the name '{name}' already exists.", 5);
return;
+ }
_currentViewName = name;
@@ -233,7 +248,16 @@ private async Task CreateNewViewAsync(string name, SaveLocationType saveLocation
private async Task CreateCopyViewAsync(string name, SaveLocationType saveLocationType)
{
if (string.IsNullOrEmpty(name))
+ {
+ _toastService?.AddToast(eToastType.Warning, "View not created", "Please enter a view name.", 5);
return;
+ }
+
+ if (_serverStorageAllViews.Contains(name) || _localStorageData.ContainsKey(name))
+ {
+ _toastService?.AddToast(eToastType.Warning, "View not created", $"A view with the name '{name}' already exists.", 5);
+ return;
+ }
var oldViewName = _currentViewName;
@@ -471,13 +495,19 @@ private async Task ImportViewAsync(InputFileChangeEventArgs e)
{
var file = e.File;
if (file == null || !file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
+ {
+ _toastService?.AddToast(eToastType.Warning, "Import failed", "Please select a valid .json file.", 5);
return;
+ }
using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024); // 10MB max
var importedView = await System.Text.Json.JsonSerializer.DeserializeAsync
(stream);
if (importedView == null)
+ {
+ _toastService?.AddToast(eToastType.Warning, "Import failed", "The file could not be deserialized.", 5);
return;
+ }
var viewName = Path.GetFileNameWithoutExtension(file.Name);
var originalName = viewName;
@@ -511,10 +541,13 @@ await Serializing.SerializeAsync(
// Load the imported view
await LoadAsync(viewName);
+ _toastService?.AddToast(eToastType.Success, "View imported", $"View '{viewName}' was imported successfully.", 5);
+
StateHasChanged();
}
catch (Exception ex)
{
+ _toastService?.AddToast(eToastType.Warning, "Import failed", $"Error importing view: {ex.Message}", 7);
Console.WriteLine($"Error importing view: {ex.Message}");
}
}
@@ -561,6 +594,8 @@ private async Task ChangeDefaultViewAsync(string view)
private List? _searchResult { get; set; } = null;
private void Search()
{
+ _searchResultPage = 1;
+
if (_searchValue is null || _searchValue == "")
{
_searchResult = null;
@@ -604,13 +639,21 @@ private void Search()
var flatChildren = obj.GetChildren().Flatten(p => p.GetChildren());
var primitives = obj.RetrievePrimitives();
- var matchingChildren = flatChildren.Where(p =>
- searchTerms.All(term =>
- p.Symbol.Contains(term, StringComparison.OrdinalIgnoreCase)));
+ var matchingChildren = _controllerObjectsFilterMode switch
+ {
+ FilterMode.StartsWith => flatChildren.Where(p => p.Symbol.StartsWith(_searchValue, StringComparison.OrdinalIgnoreCase)),
+ FilterMode.EndsWith => flatChildren.Where(p => p.Symbol.EndsWith(_searchValue, StringComparison.OrdinalIgnoreCase)),
+ FilterMode.Regex => flatChildren.Where(p => Regex.IsMatch(p.Symbol, _searchValue, RegexOptions.IgnoreCase)),
+ _ => flatChildren.Where(p => searchTerms.All(term => p.Symbol.Contains(term, StringComparison.OrdinalIgnoreCase))),
+ };
- var matchingPrimitives = primitives.Where(p =>
- searchTerms.All(term =>
- p.Symbol.Contains(term, StringComparison.OrdinalIgnoreCase)));
+ var matchingPrimitives = _controllerObjectsFilterMode switch
+ {
+ FilterMode.StartsWith => primitives.Where(p => p.Symbol.StartsWith(_searchValue, StringComparison.OrdinalIgnoreCase)),
+ FilterMode.EndsWith => primitives.Where(p => p.Symbol.EndsWith(_searchValue, StringComparison.OrdinalIgnoreCase)),
+ FilterMode.Regex => primitives.Where(p => Regex.IsMatch(p.Symbol, _searchValue, RegexOptions.IgnoreCase)),
+ _ => primitives.Where(p => searchTerms.All(term => p.Symbol.Contains(term, StringComparison.OrdinalIgnoreCase))),
+ };
_searchResult.AddRange(matchingChildren);
_searchResult.AddRange(matchingPrimitives);
@@ -625,6 +668,16 @@ private void Search()
}
private bool? _controllerObjectsSortAscending { get; set; } = null;
+ private FilterMode _controllerObjectsFilterMode { get; set; } = FilterMode.Contains;
+ private int _searchResultPage { get; set; } = 1;
+ private int _searchResultPageSize { get; set; } = 50;
+ private int _searchResultTotalCount => _searchResult?.Count ?? 0;
+
+ private IEnumerable GetPagedSearchResult()
+ {
+ if (_searchResult == null) return Enumerable.Empty();
+ return _searchResult.Skip((_searchResultPage - 1) * _searchResultPageSize).Take(_searchResultPageSize);
+ }
private void ToggleControllerObjectsSort()
{
@@ -635,6 +688,8 @@ private void ToggleControllerObjectsSort()
else if (_controllerObjectsSortAscending == false)
_controllerObjectsSortAscending = null;
+ _searchResultPage = 1;
+
// Apply sorting
if (_controllerObjectsSortAscending != null)
{
@@ -642,6 +697,8 @@ private void ToggleControllerObjectsSort()
}
}
+
+
private bool _isFileImported { get; set; } = false;
private bool _isFileImporting { get; set; } = false;
@@ -672,6 +729,7 @@ private async Task UploadFile(InputFileChangeEventArgs e)
catch (Exception ex)
{
CurrentView.ImgSrc = null;
+ _toastService?.AddToast(eToastType.Warning, "Upload failed", $"Error uploading background image: {ex.Message}", 7);
Console.WriteLine($"VisualComposer Error: {ex.Message}");
}
@@ -776,6 +834,15 @@ private void Leave(PointerEventArgs eventArgs)
}
}
+ private void Up(PointerEventArgs eventArgs)
+ {
+ foreach (var item in _items)
+ {
+ if (item.UpEvent != null)
+ item.UpEvent.Invoke(this, eventArgs);
+ }
+ }
+
private string GetBgColor(SaveLocationType location)
{
if (location == SaveLocationType.Server)
@@ -785,6 +852,42 @@ private string GetBgColor(SaveLocationType location)
return "";
}
+ private async Task LoadOptionsAsync()
+ {
+ var saved = await LocalStorage.LoadAsync(_protectedLocalStorage, OptionsStorageKey);
+ if (saved != null)
+ {
+ _options._left = saved.Left;
+ _options._top = saved.Top;
+ _options._transform = Types.TransformType.FromString(saved.Transform) ?? Types.TransformType.TopCenter;
+ _options._presentation = saved.Presentation;
+ _options._width = saved.Width;
+ _options._height = saved.Height;
+ _options._zIndex = saved.ZIndex;
+ _options._scale = saved.Scale;
+ _options._rotate = saved.Rotate;
+ _options._roles = saved.Roles;
+ _options._presentationTemplate = saved.PresentationTemplate;
+ _options._background = saved.Background;
+ _options._backgroundColorLight = saved.BackgroundColorLight;
+ _options._backgroundColorDark = saved.BackgroundColorDark;
+ _options._pollingInterval = saved.PollingInterval;
+
+ _optionsMove = saved.OptionsMove;
+ _optionsMoveDirection = saved.OptionsMoveDirection;
+ _optionsMoveBottom = saved.OptionsMoveBottom;
+ _optionsMoveRight = saved.OptionsMoveRight;
+ _customPresentation = saved.CustomPresentation;
+ }
+ }
+
+ private async Task SaveOptionsAsync()
+ {
+ var data = new SerializableControllerObjectsOptions(_options, _optionsMove, _optionsMoveDirection, _optionsMoveBottom, _optionsMoveRight, _customPresentation);
+
+ await LocalStorage.SaveAsync(_protectedLocalStorage, OptionsStorageKey, data);
+ }
+
private void ToggleSort()
{
if (_watchTableSortAscending == null)
@@ -824,5 +927,24 @@ public enum SaveLocationType
Server,
Local
}
+
+ public enum FilterMode
+ {
+ Contains,
+ StartsWith,
+ EndsWith,
+ Regex
+ }
+ }
+
+ public static class FilterModeExtensions
+ {
+ public static string ToDisplayString(this VisualComposerContainer.FilterMode mode) => mode switch
+ {
+ VisualComposerContainer.FilterMode.StartsWith => "Starts with",
+ VisualComposerContainer.FilterMode.EndsWith => "Ends with",
+ VisualComposerContainer.FilterMode.Regex => "Regex",
+ _ => "Contains",
+ };
}
-}
\ No newline at end of file
+}
diff --git a/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItem.razor.cs b/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItem.razor.cs
index 53fc42485..cf1cb1af8 100644
--- a/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItem.razor.cs
+++ b/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItem.razor.cs
@@ -21,6 +21,7 @@ protected override void OnAfterRender(bool firstRender)
{
Origin!.MoveEvent = new EventHandler((sender, e) => MoveAsync((PointerEventArgs)e));
Origin!.LeaveEvent = new EventHandler((sender, e) => Leave((PointerEventArgs)e));
+ Origin!.UpEvent = new EventHandler((sender, e) => Up((PointerEventArgs)e));
}
private async Task MoveAsync(PointerEventArgs eventArgs)
@@ -28,7 +29,7 @@ private async Task MoveAsync(PointerEventArgs eventArgs)
if (_isDragging)
{
double offsetX = ((eventArgs.ClientX - _startX) / Parent!.ElementSize.Width * 100) * (1 / Parent.CurrentView.Scale);
- double offsetY = ((eventArgs.ClientY - _startY) / Parent!.ElementSize.Width * 100) * (1 / Parent.CurrentView.Scale);
+ double offsetY = ((eventArgs.ClientY - _startY) / Parent!.ElementSize.Height * 100) * (1 / Parent.CurrentView.Scale);
Origin._left += offsetX;
Origin._top += offsetY;
diff --git a/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItemData.cs b/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItemData.cs
index b9a0871c4..2cd044bf8 100644
--- a/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItemData.cs
+++ b/src/base/src/AXOpen.VisualComposer/Components/VisualComposerItem/VisualComposerItemData.cs
@@ -15,6 +15,7 @@ public class VisualComposerItemData
public EventHandler MoveEvent { get; set; }
public EventHandler LeaveEvent { get; set; }
+ public EventHandler UpEvent { get; set; }
private ITwinElement? _twinElement;
diff --git a/src/base/src/AXOpen.VisualComposer/Serializing/SerializableControllerObjectsOptions.cs b/src/base/src/AXOpen.VisualComposer/Serializing/SerializableControllerObjectsOptions.cs
new file mode 100644
index 000000000..570ade22e
--- /dev/null
+++ b/src/base/src/AXOpen.VisualComposer/Serializing/SerializableControllerObjectsOptions.cs
@@ -0,0 +1,27 @@
+using AXOpen.VisualComposer.Components.VisualComposerItem;
+
+namespace AXOpen.VisualComposer.Serializing
+{
+ public class SerializableControllerObjectsOptions : SerializableItem
+ {
+ public bool OptionsMove { get; set; }
+ public int OptionsMoveDirection { get; set; }
+ public double OptionsMoveBottom { get; set; }
+ public double OptionsMoveRight { get; set; }
+ public bool CustomPresentation { get; set; }
+
+ public SerializableControllerObjectsOptions()
+ {
+
+ }
+
+ public SerializableControllerObjectsOptions(VisualComposerItemData visualComposerItemData, bool optionsMove, int optionsMoveDirection, double optionsMoveBottom, double optionsMoveRight, bool customPresentation) : base(visualComposerItemData)
+ {
+ OptionsMove = optionsMove;
+ OptionsMoveDirection = optionsMoveDirection;
+ OptionsMoveBottom = optionsMoveBottom;
+ OptionsMoveRight = optionsMoveRight;
+ CustomPresentation = customPresentation;
+ }
+ }
+}
diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Pages/Security.razor b/src/showcase/app/ix-blazor/showcase.blazor/Pages/Security.razor
index 6731adaf4..62f0b53a7 100644
--- a/src/showcase/app/ix-blazor/showcase.blazor/Pages/Security.razor
+++ b/src/showcase/app/ix-blazor/showcase.blazor/Pages/Security.razor
@@ -2,4 +2,4 @@
Security
-
+
diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Pages/VisualComposer.razor b/src/showcase/app/ix-blazor/showcase.blazor/Pages/VisualComposer.razor
new file mode 100644
index 000000000..92169aaff
--- /dev/null
+++ b/src/showcase/app/ix-blazor/showcase.blazor/Pages/VisualComposer.razor
@@ -0,0 +1,29 @@
+@page "/VisualComposer"
+@using AXOpen.VisualComposer.Components
+@using Microsoft.AspNetCore.Components.Authorization
+
+Visual Composer
+
+
+
+ Home
+ /
+ Visual Composer
+
+
+ AXOpen.VisualComposer
+ Visual Composer
+
+ The Visual Composer provides a drag-and-drop canvas for building custom HMI views
+ from controller objects. Create, save, and manage multiple views with background images and watch tables.
+
+
+
+
+
+
+
diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Program.cs b/src/showcase/app/ix-blazor/showcase.blazor/Program.cs
index cb5a8532e..5eba4845c 100644
--- a/src/showcase/app/ix-blazor/showcase.blazor/Program.cs
+++ b/src/showcase/app/ix-blazor/showcase.blazor/Program.cs
@@ -9,6 +9,7 @@
using AxOpen.Security.Entities;
using AxOpen.Security.Services;
using AXSharp.Connector;
+using AXOpen.VisualComposer;
using AXSharp.Presentation.Blazor.Services;
using Serilog;
using showcase;
@@ -25,6 +26,7 @@
//
builder.Services.AddIxBlazorServices();
builder.Services.AddAxoCoreServices();
+builder.Services.AddVisualComposerService();
//
builder.Services.AddSingleton();
builder.Services.AddSingleton(sp =>
@@ -186,6 +188,9 @@
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
+app.UseAuthentication();
+app.UseAuthorization();
+app.MapAdditionalIdentityEndpoints();
app.MapBlazorHub();
//
// SignalR hub for dialog/alert cross-client synchronization
diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Shared/NavMenu.razor b/src/showcase/app/ix-blazor/showcase.blazor/Shared/NavMenu.razor
index 18044985f..fd84ec043 100644
--- a/src/showcase/app/ix-blazor/showcase.blazor/Shared/NavMenu.razor
+++ b/src/showcase/app/ix-blazor/showcase.blazor/Shared/NavMenu.razor
@@ -75,6 +75,7 @@
+
diff --git a/src/showcase/app/ix-blazor/showcase.blazor/Shared/TopRow.razor b/src/showcase/app/ix-blazor/showcase.blazor/Shared/TopRow.razor
index fe0d0a7fb..6d19eaab0 100644
--- a/src/showcase/app/ix-blazor/showcase.blazor/Shared/TopRow.razor
+++ b/src/showcase/app/ix-blazor/showcase.blazor/Shared/TopRow.razor
@@ -18,7 +18,7 @@
Search
Ctrl+K
-
+