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.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 + + +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + + + Actual: +
+
+ Published: +
+
+
+
+ +
+
+
+
+
+} diff --git a/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor b/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor index 95e817d2..b1b9ccdf 100644 --- a/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor +++ b/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor @@ -75,7 +75,6 @@ Width="100%" CssClass="model-editor-details">
-

Panel Editor

@if (this.ViewModel.SelectedElementDefinition is not null) { @@ -85,7 +84,7 @@
-
+
diff --git a/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor.css b/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor.css index c81656e7..67aafaa3 100644 --- a/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor.css +++ b/COMETwebapp/Components/ModelEditor/ElementDefinitionTable.razor.css @@ -6,5 +6,5 @@ position: sticky; top: 0px; max-height: 80vh; - overflow: auto; + overflow:hidden; } diff --git a/COMETwebapp/ViewModels/Components/SystemRepresentation/ElementDefinitionDetailsViewModel.cs b/COMETwebapp/ViewModels/Components/SystemRepresentation/ElementDefinitionDetailsViewModel.cs index cbfb4096..a40c87f6 100644 --- a/COMETwebapp/ViewModels/Components/SystemRepresentation/ElementDefinitionDetailsViewModel.cs +++ b/COMETwebapp/ViewModels/Components/SystemRepresentation/ElementDefinitionDetailsViewModel.cs @@ -53,6 +53,6 @@ public ElementBase SelectedSystemNode /// /// A collection of /// - public IEnumerable Rows { get; set; } + public ICollection Rows { get; set; } } } diff --git a/COMETwebapp/ViewModels/Components/SystemRepresentation/IElementDefinitionDetailsViewModel.cs b/COMETwebapp/ViewModels/Components/SystemRepresentation/IElementDefinitionDetailsViewModel.cs index ba343ca6..f88364b1 100644 --- a/COMETwebapp/ViewModels/Components/SystemRepresentation/IElementDefinitionDetailsViewModel.cs +++ b/COMETwebapp/ViewModels/Components/SystemRepresentation/IElementDefinitionDetailsViewModel.cs @@ -40,6 +40,6 @@ public interface IElementDefinitionDetailsViewModel /// /// A collection of /// - IEnumerable Rows { get; set; } + ICollection Rows { get; set; } } } diff --git a/COMETwebapp/_Imports.razor b/COMETwebapp/_Imports.razor index 4f12a364..c8047d07 100644 --- a/COMETwebapp/_Imports.razor +++ b/COMETwebapp/_Imports.razor @@ -40,6 +40,7 @@ @using COMET.Web.Common.Components.ParameterTypeEditors @using COMET.Web.Common.Components.Selectors @using COMET.Web.Common.Components.ValueSetRenderers +@using COMET.Web.Common.Components.CardView @using COMET.Web.Common.Pages @using Blazor.Diagrams.Core; @using Blazor.Diagrams.Core.Models; diff --git a/COMETwebapp/wwwroot/css/app.css b/COMETwebapp/wwwroot/css/app.css index 2f4bcb17..e8d71525 100644 --- a/COMETwebapp/wwwroot/css/app.css +++ b/COMETwebapp/wwwroot/css/app.css @@ -302,3 +302,18 @@ sub { .validation-container:not(:has(li)) { display: none; } + +.starion-pill { + margin: 1px; + height: 19px; + width: auto; + border-radius: 30px; + background-color: white; + border: 1px solid dimgray; + color: dimgray; + padding-top: 0px; +} + +.search-mark { + background-color: yellow !important; +} diff --git a/COMETwebapp/wwwroot/css/vars.css b/COMETwebapp/wwwroot/css/vars.css index d90dde54..f4081555 100644 --- a/COMETwebapp/wwwroot/css/vars.css +++ b/COMETwebapp/wwwroot/css/vars.css @@ -59,7 +59,7 @@ --spacing-18: 4.5rem; --spacing-19: 4.75rem; --spacing-20: 5rem; - --dxbl-listbox-font-size: 0.98rem; - --bs-body-font-size: 0.98rem; + --dxbl-listbox-font-size: 0.88rem; + --bs-body-font-size: 0.88rem; --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; }