diff --git a/COMET.Web.Common.Tests/Components/CardView/CardViewTestFixture.cs b/COMET.Web.Common.Tests/Components/CardView/CardViewTestFixture.cs
new file mode 100644
index 00000000..3ba04fe4
--- /dev/null
+++ b/COMET.Web.Common.Tests/Components/CardView/CardViewTestFixture.cs
@@ -0,0 +1,484 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2023-2024 Starion Group S.A.
+//
+// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, Nabil Abbar
+//
+// This file is part of CDP4-COMET WEB Community Edition
+// The CDP4-COMET WEB Community Edition is the Starion Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C.
+//
+// The CDP4-COMET WEB Community Edition is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Affero General Public
+// License as published by the Free Software Foundation; either
+// version 3 of the License, or (at your option) any later version.
+//
+// The CDP4-COMET WEB Community Edition is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace COMET.Web.Common.Tests.Components.CardView
+{
+ using Bunit;
+
+ using COMET.Web.Common.Components.CardView;
+
+ using DevExpress.Blazor;
+ using DevExpress.Blazor.Internal;
+
+ using Microsoft.AspNetCore.Components;
+ using Microsoft.Extensions.DependencyInjection;
+
+ using NUnit.Framework;
+
+ using TestContext = Bunit.TestContext;
+
+ public class CardViewTestFixture
+ {
+ private TestContext context;
+ private TestClass testClass1 = new ();
+ private TestClass testClass2 = new ();
+ private TestClass testClass3 = new ();
+ private TestClass[] testClasses;
+ private string[] searchFields = new[] { "Id", "Name" };
+ private string[] sortFields = new[] { string.Empty, "Id", "Name" };
+
+ private static RenderFragment NormalTemplate()
+ {
+ RenderFragment Template(TestClass child) =>
+ builder =>
+ {
+ builder.OpenComponent>(0);
+ builder.AddAttribute(1, "Context", child);
+ builder.AddAttribute(2, "FieldName", "Id");
+ builder.CloseComponent();
+ builder.OpenComponent>(3);
+ builder.AddAttribute(4, "Context", child);
+ builder.AddAttribute(5, "FieldName", "Name");
+ builder.CloseComponent();
+ };
+
+ return Template;
+ }
+
+ private static RenderFragment NoSearchAndSortTemplate()
+ {
+ RenderFragment Template(TestClass child) =>
+ builder =>
+ {
+ builder.OpenComponent>(0);
+ builder.AddAttribute(1, "Context", child);
+ builder.AddAttribute(2, "FieldName", "Id");
+ builder.AddAttribute(3, "AllowSearch", false);
+ builder.AddAttribute(4, "AllowSort", false);
+ builder.CloseComponent();
+ builder.OpenComponent>(5);
+ builder.AddAttribute(6, "Context", child);
+ builder.AddAttribute(7, "FieldName", "Name");
+ builder.AddAttribute(8, "AllowSearch", false);
+ builder.AddAttribute(9, "AllowSort", false);
+ builder.CloseComponent();
+ };
+
+ return Template;
+ }
+
+ [SetUp]
+ public void Setup()
+ {
+ this.context = new TestContext();
+ this.testClasses = [this.testClass1, this.testClass2, this.testClass3];
+
+ this.context.Services.AddDevExpressBlazor(_ => ConfigureJsInterop(this.context.JSInterop));
+
+ this.context.JSInterop.SetupVoid("DxBlazor.AdaptiveDropDown.init");
+ this.context.JSInterop.SetupVoid("DxBlazor.DropDown.getReference");
+ this.context.JSInterop.SetupVoid("DxBlazor.ComboBox.loadModule");
+ this.context.JSInterop.SetupVoid("DxBlazor.Input.loadModule");
+ }
+
+ [Test]
+ public void VerifyComponent()
+ {
+ var component = this.context.RenderComponent>(parameters =>
+ {
+ parameters
+ .Add(p => p.Items, this.testClasses)
+ .Add(p => p.ItemSize, 150)
+ .Add(p => p.ItemTemplate, NormalTemplate());
+ });
+
+ var cardView = component;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardView.Instance.AllowSort, Is.True);
+ Assert.That(cardView.Instance.AllowSearch, Is.True);
+ Assert.That(cardView.Instance.ItemSize, Is.EqualTo(150));
+ Assert.That(cardView.Instance.SearchFields, Is.EquivalentTo(this.searchFields));
+ Assert.That(cardView.Instance.SortFields, Is.EquivalentTo(this.sortFields));
+ });
+
+ var textBoxParentComponent = component.Find("#search-textbox");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(textBoxParentComponent, Is.Not.Null);
+ Assert.That(textBoxParentComponent.Attributes.Single(x => x.Name == "style").Value.Contains("visibility:block"), Is.True);
+ });
+
+ var comboBoxParentComponent = component.Find("#sort-dropdown");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(comboBoxParentComponent, Is.Not.Null);
+ Assert.That(comboBoxParentComponent.Attributes.Single(x => x.Name == "style").Value.Contains("visibility:block"), Is.True);
+ });
+
+ var cardFields = component.FindComponents>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardFields.Count, Is.EqualTo(6));
+
+ foreach (var cardField in cardFields)
+ {
+ Assert.That(cardField.Instance.AllowSearch, Is.True);
+ Assert.That(cardField.Instance.AllowSort, Is.True);
+ }
+
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Id.ToString())).Count, Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public void VerifyComponentWithoutSortAndSearch()
+ {
+ var component = this.context.RenderComponent>(parameters =>
+ {
+ parameters
+ .Add(p => p.Items, this.testClasses)
+ .Add(p => p.ItemSize, 150)
+ .Add(p => p.ItemTemplate, NoSearchAndSortTemplate());
+ });
+
+ var cardView = component;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardView.Instance.AllowSort, Is.False);
+ Assert.That(cardView.Instance.AllowSearch, Is.False);
+ Assert.That(cardView.Instance.ItemSize, Is.EqualTo(150));
+ Assert.That(cardView.Instance.SearchFields.Count, Is.EqualTo(0));
+ Assert.That(cardView.Instance.SortFields.Count, Is.EqualTo(1));
+ });
+
+ var textBoxParentComponent = component.Find("#search-textbox");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(textBoxParentComponent, Is.Not.Null);
+ Assert.That(textBoxParentComponent.Attributes.Single(x => x.Name == "style").Value.Contains("visibility:hidden"), Is.True);
+ });
+
+ var comboBoxParentComponent = component.Find("#sort-dropdown");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(comboBoxParentComponent, Is.Not.Null);
+ Assert.That(comboBoxParentComponent.Attributes.Single(x => x.Name == "style").Value.Contains("visibility:hidden"), Is.True);
+ });
+
+ var cardFields = component.FindComponents>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardFields.Count, Is.EqualTo(6));
+
+ foreach (var cardField in cardFields)
+ {
+ Assert.That(cardField.Instance.AllowSearch, Is.False);
+ Assert.That(cardField.Instance.AllowSort, Is.False);
+ }
+
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Id.ToString())).Count, Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public void VerifyComponentRerenders()
+ {
+ var component = this.context.RenderComponent>(parameters =>
+ {
+ parameters
+ .Add(p => p.Items, this.testClasses)
+ .Add(p => p.ItemSize, 150)
+ .Add(p => p.ItemTemplate, NoSearchAndSortTemplate());
+ });
+
+ var cardFields = component.FindComponents>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardFields.Count, Is.EqualTo(6));
+
+ foreach (var cardField in cardFields)
+ {
+ Assert.That(cardField.Instance.AllowSearch, Is.False);
+ Assert.That(cardField.Instance.AllowSort, Is.False);
+ }
+
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Id.ToString())).Count, Is.EqualTo(1));
+ });
+
+ // create new collection of items
+ this.testClass1 = new TestClass();
+ this.testClass2 = new TestClass();
+ this.testClass3 = new TestClass();
+ this.testClasses = [this.testClass1, this.testClass2, this.testClass3];
+
+ component.SetParametersAndRender(parameters => parameters
+ .Add(p => p.Items, this.testClasses));
+
+ cardFields = component.FindComponents>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardFields.Count, Is.EqualTo(6));
+
+ foreach (var cardField in cardFields)
+ {
+ Assert.That(cardField.Instance.AllowSearch, Is.False);
+ Assert.That(cardField.Instance.AllowSort, Is.False);
+ }
+
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Id.ToString())).Count, Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public void VerifySelectComponent()
+ {
+ var component = this.context.RenderComponent>(parameters =>
+ {
+ parameters
+ .Add(p => p.Items, this.testClasses)
+ .Add(p => p.ItemSize, 150)
+ .Add(p => p.ItemTemplate, NormalTemplate());
+ });
+
+ var cardView = component;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardView.Instance.AllowSort, Is.True);
+ Assert.That(cardView.Instance.AllowSearch, Is.True);
+ Assert.That(cardView.Instance.ItemSize, Is.EqualTo(150));
+ Assert.That(cardView.Instance.SearchFields, Is.EquivalentTo(this.searchFields));
+ Assert.That(cardView.Instance.SortFields, Is.EquivalentTo(this.sortFields));
+ });
+
+ var firstCardField = component.Find(".card");
+ firstCardField.Click();
+
+ var selectedCardField = component.Find(".selected");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(firstCardField.InnerHtml, Is.EqualTo(selectedCardField.InnerHtml));
+ });
+ }
+
+ [Test]
+ public async Task VerifySearchComponent()
+ {
+ var component = this.context.RenderComponent>(parameters =>
+ {
+ parameters
+ .Add(p => p.Items, this.testClasses)
+ .Add(p => p.ItemSize, 150)
+ .Add(p => p.ItemTemplate, NormalTemplate());
+ });
+
+ var cardView = component;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardView.Instance.AllowSort, Is.True);
+ Assert.That(cardView.Instance.AllowSearch, Is.True);
+ Assert.That(cardView.Instance.ItemSize, Is.EqualTo(150));
+ Assert.That(cardView.Instance.SearchFields, Is.EquivalentTo(this.searchFields));
+ Assert.That(cardView.Instance.SortFields, Is.EquivalentTo(this.sortFields));
+ });
+
+ var textBoxParentComponent = component.Find("#search-textbox");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(textBoxParentComponent, Is.Not.Null);
+ Assert.That(textBoxParentComponent.Attributes.Single(x => x.Name == "style").Value.Contains("visibility:block"), Is.True);
+ });
+
+ var textBoxComponent = component.FindComponent();
+ await component.InvokeAsync(() => textBoxComponent.Instance.TextChanged.InvokeAsync(this.testClass1.Id.ToString()));
+
+ var cardFields = component.FindComponents>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardFields.Count, Is.EqualTo(2));
+
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Name)).ToList().Count, Is.EqualTo(0));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Name)).ToList().Count, Is.EqualTo(0));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Id.ToString()) && x.Markup.Contains("search-mark")).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Id.ToString())).ToList().Count, Is.EqualTo(0));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Id.ToString())).Count, Is.EqualTo(0));
+ });
+
+ await component.InvokeAsync(async () => await textBoxComponent.Instance.TextChanged.InvokeAsync(string.Empty));
+
+ cardFields = component.FindComponents>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardFields.Count, Is.EqualTo(6));
+
+ Assert.That(cardFields.Where(x => x.Markup.Contains("search-mark")).ToList().Count, Is.EqualTo(0));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Id.ToString())).Count, Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public async Task VerifySortComponent()
+ {
+ var component = this.context.RenderComponent>(parameters =>
+ {
+ parameters
+ .Add(p => p.Items, this.testClasses)
+ .Add(p => p.ItemSize, 150)
+ .Add(p => p.ItemTemplate, NormalTemplate());
+ });
+
+ var cardView = component;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardView.Instance.AllowSort, Is.True);
+ Assert.That(cardView.Instance.AllowSearch, Is.True);
+ Assert.That(cardView.Instance.ItemSize, Is.EqualTo(150));
+ Assert.That(cardView.Instance.SearchFields, Is.EquivalentTo(this.searchFields));
+ Assert.That(cardView.Instance.SortFields, Is.EquivalentTo(this.sortFields));
+ Assert.That(cardView.Instance.SelectedSortField == string.Empty);
+ });
+
+ var sortParentComponent = component.Find("#sort-dropdown");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(sortParentComponent, Is.Not.Null);
+ Assert.That(sortParentComponent.Attributes.Single(x => x.Name == "style").Value.Contains("visibility:block"), Is.True);
+ });
+
+ var cardFields = component.FindComponents>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardFields.Count, Is.EqualTo(6));
+
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Name)).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass1.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass2.Id.ToString())).ToList().Count, Is.EqualTo(1));
+ Assert.That(cardFields.Where(x => x.Markup.Contains(this.testClass3.Id.ToString())).Count, Is.EqualTo(1));
+ });
+
+ await component.InvokeAsync(() => component.Instance.OnSelectedSortItemChanged("Id"));
+
+ component.Render();
+
+ cardFields = component.FindComponents>();
+
+ //var sortedTestClasses = this.testClasses.OrderBy(x => x.Id.ToString()).Select(x => x.Name).ToList();
+ var sortedTestClasses = this.testClasses.OrderBy(x => x.Id).Select(x => x.Name).ToList();
+ var sortedCarFields = cardFields.Where(x => x.Markup.StartsWith("Name-")).Select(x => x.Markup).ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(component.Instance.SelectedSortField == "Id");
+ Assert.That(sortedTestClasses[0], Is.EqualTo(sortedCarFields[0]));
+ Assert.That(sortedTestClasses[1], Is.EqualTo(sortedCarFields[1]));
+ Assert.That(sortedTestClasses[2], Is.EqualTo(sortedCarFields[2]));
+ });
+
+ await component.InvokeAsync(() => component.Instance.OnSelectedSortItemChanged(string.Empty));
+
+ component.Render();
+
+ cardFields = component.FindComponents>();
+
+ sortedCarFields = cardFields.Where(x => x.Markup.StartsWith("Name-")).Select(x => x.Markup).ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(component.Instance.SelectedSortField == string.Empty);
+ Assert.That(this.testClasses[0].Name, Is.EqualTo(sortedCarFields[0]));
+ Assert.That(this.testClasses[1].Name, Is.EqualTo(sortedCarFields[1]));
+ Assert.That(this.testClasses[2].Name, Is.EqualTo(sortedCarFields[2]));
+ });
+ }
+
+ ///
+ /// Configure the for DevExpress
+ ///
+ /// The to configure
+ private static void ConfigureJsInterop(BunitJSInterop interop)
+ {
+ interop.Mode = JSRuntimeMode.Loose;
+
+ var rootModule = interop.SetupModule("./_content/DevExpress.Blazor/dx-blazor.js");
+ rootModule.Mode = JSRuntimeMode.Strict;
+
+ rootModule.Setup("getDeviceInfo", _ => true)
+ .SetResult(new DeviceInfo(false));
+ }
+ }
+
+ public class TestClass
+ {
+ public Guid Id { get; } = Guid.NewGuid();
+
+ public string Name { get; } = $"Name-{DateTime.Now.Ticks}";
+ }
+}
diff --git a/COMET.Web.Common/COMET.Web.Common.csproj b/COMET.Web.Common/COMET.Web.Common.csproj
index 90d67d8b..b35f7bfa 100644
--- a/COMET.Web.Common/COMET.Web.Common.csproj
+++ b/COMET.Web.Common/COMET.Web.Common.csproj
@@ -36,6 +36,7 @@
+
@@ -43,6 +44,7 @@
+
diff --git a/COMET.Web.Common/Components/CardView/CardField.cs b/COMET.Web.Common/Components/CardView/CardField.cs
new file mode 100644
index 00000000..68ff33ac
--- /dev/null
+++ b/COMET.Web.Common/Components/CardView/CardField.cs
@@ -0,0 +1,141 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2023-2024 Starion Group S.A.
+//
+// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, Nabil Abbar
+//
+// This file is part of CDP4-COMET WEB Community Edition
+// The CDP4-COMET WEB Community Edition is the Starion Web Application implementation of ECSS-E-TM-10-25
+// Annex A and Annex C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace COMET.Web.Common.Components.CardView
+{
+ using System.Text.RegularExpressions;
+
+ using FastMember;
+
+ using Microsoft.AspNetCore.Components;
+ using Microsoft.AspNetCore.Components.Rendering;
+
+ ///
+ /// A component that represents a data field in a 's ItemTemplate
+ ///
+ ///
+ public class CardField : ComponentBase
+ {
+ ///
+ /// The used to read properties from the instance of T.
+ /// This is a static property on a generic type, so it will have different static values for each used generic type in the application
+ ///
+ private static TypeAccessor typeAccessor;
+
+ ///
+ /// Initializes the static properties of this class
+ ///
+ static CardField()
+ {
+ typeAccessor = TypeAccessor.Create(typeof(T));
+ }
+
+ ///
+ /// Gets or sets The parent t
+ ///
+ [CascadingParameter(Name="CardView")]
+ private CardView CardView { get; set; }
+
+ ///
+ /// The SearchTerm of the used to visually show the SearchTerm in this
+ ///
+ [CascadingParameter(Name = "SearchTerm")]
+ private string SearchTerm { get; set; }
+
+ ///
+ /// Gets or sets the context of this
+ ///
+ [Parameter]
+ public T Context { get; set; }
+
+ ///
+ /// Gets or sets the FieldName (propertyname of T) to show in the UI
+ ///
+ [Parameter]
+ public string FieldName { get; set; }
+
+ ///
+ /// Gets or sets a value indicating that sorting is allowed for this
+ ///
+ [Parameter]
+ public bool AllowSort { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating that searching is allowed for this
+ ///
+ [Parameter]
+ public bool AllowSearch { get; set; } = true;
+
+ ///
+ /// Method invoked when the component is ready to start, having received its
+ /// initial parameters from its parent in the render tree.
+ ///
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+ this.CardView.InitializeCardField(this);
+ }
+
+ ///
+ /// Renders the component to the supplied .
+ ///
+ /// A that will receive the render output.
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ base.BuildRenderTree(builder);
+ var value = typeAccessor[this.Context, this.FieldName].ToString();
+
+ if (this.AllowSearch && !string.IsNullOrWhiteSpace(value) && !string.IsNullOrWhiteSpace(this.SearchTerm))
+ {
+ var separatorPattern = $"({this.SearchTerm})";
+ var result = Regex.Split(value, separatorPattern, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(30));
+ var elementCounter = 0;
+
+ foreach (var element in result)
+ {
+ if (string.Equals(element, this.SearchTerm, StringComparison.OrdinalIgnoreCase))
+ {
+ builder.OpenElement(elementCounter, "span");
+ elementCounter++;
+ builder.AddAttribute(elementCounter, "class", "search-mark");
+ elementCounter++;
+ builder.AddContent(elementCounter, element);
+ elementCounter++;
+ builder.CloseElement();
+ }
+ else
+ {
+ builder.AddContent(elementCounter, element);
+ elementCounter++;
+ }
+ }
+ }
+ else
+ {
+ builder.AddContent(0, value);
+ }
+ }
+ }
+}
diff --git a/COMET.Web.Common/Components/CardView/CardView.razor b/COMET.Web.Common/Components/CardView/CardView.razor
new file mode 100644
index 00000000..7bf4a1ab
--- /dev/null
+++ b/COMET.Web.Common/Components/CardView/CardView.razor
@@ -0,0 +1,61 @@
+
+
+@namespace COMET.Web.Common.Components.CardView
+@typeparam T
+@inherits DisposableComponent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
this.SelectItem(@context))">
+
+
+
+ @this.ItemTemplate(@context)
+
+
+
+
+
+
+
+
+
diff --git a/COMET.Web.Common/Components/CardView/CardView.razor.cs b/COMET.Web.Common/Components/CardView/CardView.razor.cs
new file mode 100644
index 00000000..3c868352
--- /dev/null
+++ b/COMET.Web.Common/Components/CardView/CardView.razor.cs
@@ -0,0 +1,236 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) 2023-2024 Starion Group S.A.
+//
+// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, Nabil Abbar
+//
+// This file is part of CDP4-COMET WEB Community Edition
+// The CDP4-COMET WEB Community Edition is the Starion Web Application implementation of ECSS-E-TM-10-25
+// Annex A and Annex C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace COMET.Web.Common.Components.CardView
+{
+ using System.Linq.Dynamic.Core;
+
+ using FastMember;
+
+ using Microsoft.AspNetCore.Components;
+ using Microsoft.AspNetCore.Components.Web.Virtualization;
+
+ ///
+ /// Component used to show a CardView based on a specific type
+ ///
+ public partial class CardView : DisposableComponent
+ {
+ ///
+ /// Gets or sets the item template for the list.
+ ///
+ [Parameter]
+ public RenderFragment? ItemTemplate { get; set; }
+
+ ///
+ /// Gets or sets the list of items of type T to use
+ ///
+ [Parameter]
+ public ICollection Items { get; set; } = new List();
+
+ ///
+ /// Gets or sets the fixed height of a Card, used to calculate the amout of items to load into the DOM in px
+ ///
+ [Parameter]
+ public float ItemSize { get; set; } = 150;
+
+ ///
+ /// Gets or sets the minimum width of a Card
+ ///
+ [Parameter]
+ public float MinWidth { get; set; } = 250;
+
+ ///
+ /// Gets or sets a collection of propertynames of type T to perform search on
+ ///
+ public HashSet SearchFields { get; private set; } = [];
+
+ ///
+ /// Gets or sets a collection of propertynames of type T to perform sorting on
+ ///
+ public SortedSet SortFields { get; private set; } = [string.Empty];
+
+ ///
+ /// Gets or sets a value indication that sorting is allowed
+ ///
+ public bool AllowSort { get; set; }
+
+ ///
+ /// Gets or sets a value indication that searching is allowed
+ ///
+ public bool AllowSearch { get; set; }
+
+ ///
+ /// hold a reference to the previously selected Item.
+ /// Typically used to check for changes in Items collection
+ ///
+ private ICollection previousItems;
+
+ ///
+ /// A reference to the component for loading items
+ ///
+ private Virtualize? virtualize; // Reference to the Virtualize component
+
+ ///
+ /// The FastMember to use to perform actions on instances of type T
+ ///
+ private TypeAccessor typeAccessor = TypeAccessor.Create(typeof(T));
+
+ ///
+ /// The selected Card in the CardView
+ ///
+ private T selected;
+
+ ///
+ /// Gets or sets the term where to search/filter items on
+ ///
+ public string SearchTerm { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the term where to sort items on
+ ///
+ public string SelectedSortField { get; set; } = string.Empty;
+
+ ///
+ /// Gets the class to visually show a Card to be selected or unselected
+ ///
+ ///
+ /// A string that retrurns the css class for selected Card
+ private string GetSelectedClass(T vm)
+ {
+ return vm.Equals(this.selected) ? "selected" : "";
+ }
+
+ ///
+ /// Set the selected item
+ ///
+ /// The item of type T
+ public void SelectItem(T item)
+ {
+ this.selected = item;
+ }
+
+ ///
+ /// Filters the list of items to show in the UI based on the
+ ///
+ /// The request to perform filtering of the items list
+ /// an waitable
+ private ValueTask> LoadItems(ItemsProviderRequest request)
+ {
+ // Filter items based on the SearchTerm
+ var filteredItems = !this.AllowSearch || string.IsNullOrWhiteSpace(this.SearchTerm)
+ ? this.Items
+ : this.Items.Where(item => this.FilterItem(item, this.SearchTerm)).ToList();
+
+ // Return paged items for virtualization
+ var items = filteredItems.Skip(request.StartIndex).Take(request.Count);
+
+ if (this.AllowSort && !string.IsNullOrWhiteSpace(this.SelectedSortField))
+ {
+ items = items.AsQueryable().OrderBy(this.SelectedSortField);
+ }
+
+ return new ValueTask>(new ItemsProviderResult(items.ToList(), filteredItems.Count));
+ }
+
+ ///
+ /// Used to filter items based on the
+ ///
+ /// The item to perform searching on
+ /// The string to search for
+ /// True if the item's selected properties contain the value to search for, otherwise false
+ private bool FilterItem(T item, string query)
+ {
+ if (this.AllowSearch)
+ {
+ var props = this.typeAccessor.GetMembers();
+
+ return props.Where(x => this.SearchFields.Contains(x.Name)).Any(prop =>
+ {
+ var value = this.typeAccessor[item, prop.Name].ToString();
+ return value != null && value.Contains(query, StringComparison.OrdinalIgnoreCase);
+ });
+ }
+
+ return true;
+ }
+
+ ///
+ /// A method that is executed when the user changes the search input element
+ ///
+ /// The text from the UI element's event
+ private void OnSearchTextChanged(string value)
+ {
+ this.SearchTerm = value ?? string.Empty;
+
+ this.virtualize?.RefreshDataAsync(); // Tell Virtualize to refresh data
+ }
+
+ ///
+ /// Initializes the
+ ///
+ /// the
+ internal void InitializeCardField(CardField cardField)
+ {
+ if (cardField.AllowSort && this.SortFields.Add(cardField.FieldName))
+ {
+ this.AllowSort = true;
+ this.StateHasChanged();
+ }
+
+ if (cardField.AllowSearch && this.SearchFields.Add(cardField.FieldName))
+ {
+ this.AllowSearch = true;
+ this.StateHasChanged();
+ }
+ }
+
+ ///
+ /// Handles the selection of a the Sort item
+ ///
+ ///
+ public void OnSelectedSortItemChanged(string newVal)
+ {
+ this.SelectedSortField = newVal ?? string.Empty;
+
+ this.virtualize?.RefreshDataAsync(); // Tell Virtualize to refresh data
+ }
+
+ ///
+ /// Raised when a parameter is set
+ ///
+ /// An awaitable
+ protected override async Task OnParametersSetAsync()
+ {
+ await base.OnParametersSetAsync();
+
+ if (!Equals(this.previousItems, this.Items) && this.virtualize != null)
+ {
+ await this.virtualize.RefreshDataAsync(); // Tell Virtualize to refresh data
+ }
+
+ this.previousItems = this.Items;
+ }
+ }
+}
diff --git a/COMET.Web.Common/Components/CardView/CardView.razor.css b/COMET.Web.Common/Components/CardView/CardView.razor.css
new file mode 100644
index 00000000..4be8b3eb
--- /dev/null
+++ b/COMET.Web.Common/Components/CardView/CardView.razor.css
@@ -0,0 +1,12 @@
+.card {
+ margin-bottom: 10px;
+ background-color: #f9f9fb;
+}
+
+.card:hover {
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.card.selected {
+ border: 2px solid var(--bs-primary, var(--primary));
+}
diff --git a/COMET.Web.Common/_Imports.razor b/COMET.Web.Common/_Imports.razor
index e2da291b..ecb41a86 100644
--- a/COMET.Web.Common/_Imports.razor
+++ b/COMET.Web.Common/_Imports.razor
@@ -2,6 +2,7 @@
@using COMET.Web.Common.Components.Applications
@using COMET.Web.Common.Components.Selectors
@using COMET.Web.Common.Components.ParameterTypeEditors
+@using COMET.Web.Common.Components.CardView
@using COMET.Web.Common.Shared
@using COMET.Web.Common.Shared.TopMenuEntry
@using System.Net.Http
diff --git a/COMET.Web.Common/wwwroot/css/styles.css b/COMET.Web.Common/wwwroot/css/styles.css
index 719b4eed..83c84789 100644
--- a/COMET.Web.Common/wwwroot/css/styles.css
+++ b/COMET.Web.Common/wwwroot/css/styles.css
@@ -572,3 +572,38 @@
width: 25%;
margin: 0;
}
+
+.inline-search-icon {
+ display: flex;
+ align-items: center;
+ position: relative;
+ padding-left: 20px !important; /* Leave space for the icon */
+}
+
+ .inline-search-icon::before {
+ content: '\e08f'; /* Unicode for an example icon (change this for your preferred icon) */
+ font-family: Icons;
+ top:3px;
+ position: absolute;
+ left: 5px; /* Position of the icon */
+ color: #757575; /* Icon color */
+ font-size: 16px; /* Icon size */
+ }
+
+.inline-sort-icon {
+ display: flex;
+ align-items: center;
+ position: relative;
+ padding-left: 20px !important; /* Leave space for the icon */
+}
+
+ .inline-sort-icon::before {
+ content: '\e0bf'; /* Unicode for an example icon (change this for your preferred icon) */
+ font-family: Icons;
+ vertical-align: middle;
+ position: absolute;
+ top: 3px;
+ left: 5px; /* Position of the icon */
+ color: #757575; /* Icon color */
+ font-size: 16px; /* Icon size */
+ }
\ No newline at end of file
diff --git a/COMETwebapp/Components/Common/DataItemDetailsComponent.razor.css b/COMETwebapp/Components/Common/DataItemDetailsComponent.razor.css
index b737c6d7..013c5918 100644
--- a/COMETwebapp/Components/Common/DataItemDetailsComponent.razor.css
+++ b/COMETwebapp/Components/Common/DataItemDetailsComponent.razor.css
@@ -1,5 +1,5 @@
.data-item-details-section {
- padding: 30px;
+ padding: 15px;
border: 1px dashed;
border-radius: 10px;
height: 100%;
diff --git a/COMETwebapp/Components/ModelEditor/DetailsPanelEditor.razor b/COMETwebapp/Components/ModelEditor/DetailsPanelEditor.razor
index 2d260b0e..8ef86c23 100644
--- a/COMETwebapp/Components/ModelEditor/DetailsPanelEditor.razor
+++ b/COMETwebapp/Components/ModelEditor/DetailsPanelEditor.razor
@@ -28,15 +28,38 @@
@if (this.ViewModel.SelectedSystemNode != null)
{
-
-
-
-
-
-
-
-
-
-
-
-}
\ No newline at end of file
+
+
+