diff --git a/.gitignore b/.gitignore index 555c6309..d93cc446 100644 --- a/.gitignore +++ b/.gitignore @@ -349,3 +349,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ /COMETwebapp/Tools + +# Jetbrains folders +.idea/ diff --git a/COMET.Web.Common.Tests/Components/BookEditor/EditotPopupTestFixture.cs b/COMET.Web.Common.Tests/Components/BookEditor/EditotPopupTestFixture.cs index 81cb95cb..a0ff55e9 100644 --- a/COMET.Web.Common.Tests/Components/BookEditor/EditotPopupTestFixture.cs +++ b/COMET.Web.Common.Tests/Components/BookEditor/EditotPopupTestFixture.cs @@ -31,6 +31,7 @@ namespace COMET.Web.Common.Tests.Components.BookEditor using COMET.Web.Common.Components; using COMET.Web.Common.Components.BookEditor; + using COMET.Web.Common.Services.SessionManagement; using COMET.Web.Common.Test.Helpers; using COMET.Web.Common.ViewModels.Components.BookEditor; @@ -40,6 +41,7 @@ namespace COMET.Web.Common.Tests.Components.BookEditor using DynamicData; using Microsoft.AspNetCore.Components; + using Microsoft.Extensions.DependencyInjection; using Moq; @@ -58,12 +60,15 @@ public class EditotPopupTestFixture private List availableCategories; private bool onCancelCalled; private bool onAcceptCalled; + private Mock sessionService; [SetUp] public void Setup() { this.context = new TestContext(); this.context.ConfigureDevExpressBlazor(); + this.sessionService = new Mock(); + this.context.Services.AddSingleton(this.sessionService.Object); this.book = new Book(); diff --git a/COMET.Web.Common.Tests/Components/BookEditor/InputEditorTestFixture.cs b/COMET.Web.Common.Tests/Components/BookEditor/InputEditorTestFixture.cs index fab129e2..bd9de519 100644 --- a/COMET.Web.Common.Tests/Components/BookEditor/InputEditorTestFixture.cs +++ b/COMET.Web.Common.Tests/Components/BookEditor/InputEditorTestFixture.cs @@ -24,18 +24,28 @@ namespace COMET.Web.Common.Tests.Components.BookEditor { + using System.Text.Json; + using Bunit; using CDP4Common.ReportingData; using CDP4Common.SiteDirectoryData; + using COMET.Web.Common.Components; using COMET.Web.Common.Components.BookEditor; + using COMET.Web.Common.Services.SessionManagement; using COMET.Web.Common.Test.Helpers; using DevExpress.Blazor; - + + using Microsoft.Extensions.DependencyInjection; + + using Moq; + using NUnit.Framework; + using RichardSzalay.MockHttp; + using TestContext = Bunit.TestContext; [TestFixture] @@ -46,12 +56,26 @@ public class InputEditorTestFixture private Book book; private List activeDomains; private List availableCategories; - + private Mock sessionService; + private MockHttpMessageHandler mockHttpMessageHandler; + private HttpClient httpClient; + private const string BookName = "Book Example"; + private const string BookShortName = "bookExample"; + [SetUp] public void Setup() { this.context = new TestContext(); this.context.ConfigureDevExpressBlazor(); + this.sessionService = new Mock(); + this.context.Services.AddSingleton(this.sessionService.Object); + this.mockHttpMessageHandler = new MockHttpMessageHandler(); + this.httpClient = this.mockHttpMessageHandler.ToHttpClient(); + this.httpClient.BaseAddress = new Uri("http://localhost/"); + this.context.Services.AddScoped(_ => this.httpClient); + var httpResponse = new HttpResponseMessage(); + httpResponse.Content = new StringContent("{\n \"ShowName\": true,\n \"ShowShortName\" : true \n}\n"); + this.mockHttpMessageHandler.When(HttpMethod.Get, "/_content/CDP4.WEB.Common/BookInputConfiguration.json").Respond(_ => httpResponse); this.activeDomains = new List { @@ -65,8 +89,8 @@ public void Setup() this.book = new Book() { - Name = "Book Example", - ShortName = "bookExample", + Name = BookName, + ShortName = BookShortName, Owner = this.activeDomains.First(), Category = this.availableCategories }; @@ -82,33 +106,27 @@ public void Setup() [Test] public void VerifyComponent() { - var basicTab = this.component.Find(".basic-tab"); - basicTab.Click(); - var textboxes = this.component.FindComponents(); var combobox = this.component.FindComponent>(); + var categoryComboBox = this.component.FindComponent>(); - var nameTextbox = textboxes[0]; - var shortNameTextbox = textboxes[1]; - + var nameTextbox = textboxes.FirstOrDefault(x => x.Instance.Text == BookName); + var shortNameTextbox = textboxes.FirstOrDefault(x => x.Instance.Text == BookShortName); + Assert.Multiple(() => { - Assert.That(nameTextbox.Instance.Text, Is.EqualTo("Book Example")); - Assert.That(shortNameTextbox.Instance.Text, Is.EqualTo("bookExample")); + Assert.IsNotNull(nameTextbox); + Assert.IsNotNull(shortNameTextbox); Assert.That(combobox.Instance.Value, Is.EqualTo(this.activeDomains.First())); + Assert.That(categoryComboBox.Instance, Is.Not.Null); }); - - var categoryTab = this.component.Find(".category-tab"); - categoryTab.Click(); - + this.component.Render(); - - var listbox = this.component.FindComponent>(); - + Assert.Multiple(() => { - Assert.That(listbox.Instance.Data, Is.EquivalentTo(this.availableCategories)); - Assert.That(listbox.Instance.Values, Is.EquivalentTo(this.availableCategories)); + Assert.That(categoryComboBox.Instance.Data, Is.EquivalentTo(this.availableCategories)); + Assert.That(categoryComboBox.Instance.Values, Is.EquivalentTo(this.availableCategories)); }); } } diff --git a/COMET.Web.Common.Tests/Components/MultiComboBoxTestFixture.cs b/COMET.Web.Common.Tests/Components/MultiComboBoxTestFixture.cs new file mode 100644 index 00000000..c998a6b3 --- /dev/null +++ b/COMET.Web.Common.Tests/Components/MultiComboBoxTestFixture.cs @@ -0,0 +1,109 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the RHEA Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The 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 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 +{ + using Bunit; + + using CDP4Common.DTO; + + using COMET.Web.Common.Components; + using COMET.Web.Common.Test.Helpers; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components; + + using TestContext = Bunit.TestContext; + + using NUnit.Framework; + + [TestFixture] + public class MultiComboBoxTestFixture + { + private TestContext context; + private IRenderedComponent> component; + private List availableCategories; + + [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.context.ConfigureDevExpressBlazor(); + + this.availableCategories = new List + { + new() { Name = "Category" }, + new() { Name = "Category2" }, + new() { Name = "Category3" }, + new() { Name = "Category4" }, + new() { Name = "Category5" }, + }; + + this.component = this.context.RenderComponent>(parameter => + { + parameter.Add(p => p.Data, this.availableCategories); + parameter.Add(p => p.Values, this.availableCategories); + parameter.Add(p => p.ShowCheckBoxes, true); + parameter.Add(p => p.MaxNumberOfChips, 2); + parameter.Add(p => p.Enabled, true); + + parameter.Add(p => p.EditorTextTemplate, builder => + { + builder.OpenElement(0, "span"); + builder.AddContent(1, ""); + builder.CloseElement(); + }); + + parameter.Add(p => p.RowTemplate, value => value.Name); + }); + } + + [Test] + public async Task VerifyComponent() + { + Assert.Multiple(() => + { + Assert.IsTrue(this.component.Instance.Enabled); + Assert.IsNotEmpty(this.component.Instance.Data); + Assert.IsNotEmpty(this.component.Instance.Values); + Assert.IsTrue(this.component.Instance.ShowCheckBoxes); + Assert.IsNotNull(this.component.Instance.EditorTextTemplate); + Assert.That(this.component.Instance.MaxNumberOfChips, Is.EqualTo(2)); + Assert.IsNotNull(this.component.Instance.RowTemplate); + }); + + var comboBox = this.component.FindComponent>(); + Assert.IsNotNull(comboBox); + Assert.IsNull(comboBox.Instance.Value); + + await this.component.InvokeAsync(() => comboBox.Instance.ShowDropDown()); + + var dropdownItems = this.component.FindAll(".item-template-checkbox"); + Assert.IsNotNull(dropdownItems); + Assert.IsNotEmpty(dropdownItems); + Assert.AreEqual(this.availableCategories.Count, dropdownItems.Count); + } + } +} \ No newline at end of file diff --git a/COMET.Web.Common/COMET.Web.Common.csproj b/COMET.Web.Common/COMET.Web.Common.csproj index 246988fb..7890b639 100644 --- a/COMET.Web.Common/COMET.Web.Common.csproj +++ b/COMET.Web.Common/COMET.Web.Common.csproj @@ -47,6 +47,9 @@ Always + + Always + Always diff --git a/COMET.Web.Common/Components/BookEditor/InputEditor.razor b/COMET.Web.Common/Components/BookEditor/InputEditor.razor index 1d83734f..37eda25a 100644 --- a/COMET.Web.Common/Components/BookEditor/InputEditor.razor +++ b/COMET.Web.Common/Components/BookEditor/InputEditor.razor @@ -27,67 +27,58 @@ @using CDP4Common.ReportingData @typeparam TItem -
- - -
- @if (this.Item is INamedThing namedThing) - { -
-

Name:

- -
- } +
+ @if (this.Item is INamedThing namedThing && this.showName) + { +
+

Name:

+ +
+ } - @if (this.Item is IShortNamedThing shortNamedThing) - { -
-

ShortName:

- -
- } + @if (this.Item is IShortNamedThing shortNamedThing && this.showShortName) + { +
+

ShortName:

+ +
+ } - @if (this.Item is IOwnedThing ownedThing) - { -
-

Owner:

- -
- } + @if (this.Item is IOwnedThing ownedThing) + { +
+

Owner:

+ +
+ } - @if (this.Item is TextualNote textualNote) - { -
-

Content:

- -
- } -
- - -
- @if (this.Item is ICategorizableThing categorizableThing) - { - - - } -
-
- + @if (this.Item is TextualNote textualNote) + { +
+

Content:

+ +
+ } + + @if (this.Item is ICategorizableThing categorizableThing) + { +
+

Category:

+ + + @context.Name + + +
+ }
diff --git a/COMET.Web.Common/Components/BookEditor/InputEditor.razor.cs b/COMET.Web.Common/Components/BookEditor/InputEditor.razor.cs index 0f6c2ac5..4021b281 100644 --- a/COMET.Web.Common/Components/BookEditor/InputEditor.razor.cs +++ b/COMET.Web.Common/Components/BookEditor/InputEditor.razor.cs @@ -24,25 +24,57 @@ // -------------------------------------------------------------------------------------------------------------------- namespace COMET.Web.Common.Components.BookEditor -{ - using CDP4Common.EngineeringModelData; +{ + using System.Text.Json; + + using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; + using COMET.Web.Common.Model; + using COMET.Web.Common.Services.SessionManagement; + using COMET.Web.Common.Utilities; + using Microsoft.AspNetCore.Components; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; /// /// Support class for the InputEditor component /// public partial class InputEditor { + /// + /// Gets or sets the + /// + [Inject] + public ILogger> Logger { get; set; } + + /// + /// Gets or sets the + /// + [Inject] + public HttpClient HttpClient { get; set; } + + /// + /// Gets or sets the + /// + [Inject] + public IOptions Options { get; set; } + + /// + /// Gets or sets the + /// + [Inject] + public ISessionService SessionService { get; set; } + /// /// Gets or sets the item for which the input is being provided /// [Parameter] public TItem Item { get; set; } - /// - /// Gets or sets the active + /// + /// Gets or sets the active /// [Parameter] public IEnumerable ActiveDomains { get; set; } @@ -52,7 +84,79 @@ public partial class InputEditor /// [Parameter] public IEnumerable AvailableCategories { get; set; } - + + /// + /// Sets if the component should show the name field + /// + private bool showName; + + /// + /// The name of the ShowName property on the configuration file + /// + private const string showNameConfigurationProperty = "ShowName"; + + /// + /// Sets if the component should show the shorname field + /// + private bool showShortName; + + /// + /// The name of the ShowShortName property on the configuration file + /// + private const string showShortNameConfigurationProperty = "ShowShortName"; + + /// + /// Method invoked when the component is ready to start, having received its + /// initial parameters from its parent in the render tree. + /// + /// Override this method if you will perform an asynchronous operation and + /// want the component to refresh when that operation is completed. + /// + /// A representing any asynchronous operation. + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + var jsonFile = this.Options.Value.JsonConfigurationFile ?? "BookInputConfiguration.json"; + + try + { + var configurations = await this.GetBookInputConfigurationAsync(jsonFile); + + if (configurations.TryGetValue(showNameConfigurationProperty, out var showNameValue)) + { + this.showName = showNameValue; + } + + if (configurations.TryGetValue(showShortNameConfigurationProperty, out var showShortNameValue)) + { + this.showShortName = showShortNameValue; + } + + if (this.Item is IOwnedThing ownedThing) + { + ownedThing.Owner = this.SessionService.Session.ActivePerson.DefaultDomain; + } + } + catch (Exception e) + { + this.Logger.LogError(e, "Error while getting the configuration file."); + } + } + + /// + /// Acquires the BookInput configurations + /// + /// The file name that contains the configurations + /// A KeyValuePair collection with each available configuration + private async Task> GetBookInputConfigurationAsync(string fileName) + { + var path = ContentPathBuilder.BuildPath(fileName); + var jsonContent = await this.HttpClient.GetStreamAsync(path); + var configurations = JsonSerializer.Deserialize>(jsonContent); + return configurations; + } + /// /// Handler for when the selected categories changed /// diff --git a/COMET.Web.Common/Components/MultiComboBox.razor b/COMET.Web.Common/Components/MultiComboBox.razor new file mode 100644 index 00000000..926ad8e3 --- /dev/null +++ b/COMET.Web.Common/Components/MultiComboBox.razor @@ -0,0 +1,83 @@ + +@typeparam TItem + + + +
+ @if (this.ShowCheckBoxes) + { + var isSelected = this.Values.Contains(context); + + } + + @if (this.RowTemplate != null) + { + @this.RowTemplate(context) + } + else + { + @context.ToString() + } +
+
+ + + @if(this.Values.Count <= this.MaxNumberOfChips) + { +
+ @foreach(var value in this.Values) + { +
+ @if (this.RowTemplate != null) + { + @this.RowTemplate(value) + } + else + { + @value + } + +
+ } +
+ } + else + { + if (this.EditorTextTemplate != null) + { + @this.EditorTextTemplate + } + else + { +
@(this.Values.Count) items are selected
+ } + } +
+
\ No newline at end of file diff --git a/COMET.Web.Common/Components/MultiComboBox.razor.cs b/COMET.Web.Common/Components/MultiComboBox.razor.cs new file mode 100644 index 00000000..2466e144 --- /dev/null +++ b/COMET.Web.Common/Components/MultiComboBox.razor.cs @@ -0,0 +1,109 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023 RHEA System S.A. +// +// Authors: Sam Gerené, Jaime Bernar +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the RHEA 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 +{ + using Microsoft.AspNetCore.Components; + + /// + /// Support class for the multicombobox component + /// + public partial class MultiComboBox + { + /// + /// The last selected value of the combobox + /// + private TItem lastSelectedValue; + + /// + /// Gets or sets the maximum number of chips that the combo should show + /// + [Parameter] + public int MaxNumberOfChips { get; set; } = 3; + + /// + /// Gets or sets if the checkboxes for the selected items should be drawn + /// + [Parameter] + public bool ShowCheckBoxes { get; set; } = true; + + /// + /// Gets or sets the item template for the selected items + /// + [Parameter] + public RenderFragment RowTemplate { get; set; } + + /// + /// Gets or sets item template to show when the number of selected items is greater or equal to the + /// + [Parameter] + public RenderFragment EditorTextTemplate { get; set; } + + /// + /// Gets or sets the data of the combobox + /// + [Parameter] + public IEnumerable Data { get; set; } = Enumerable.Empty(); + + /// + /// Gets or sets if the component should show all the fields as readonly + /// + [Parameter] + public bool Enabled { get; set; } = true; + + /// + /// Gets or sets the value of the selected items + /// + [Parameter] + public List Values { get; set; } = new(); + + /// + /// Gets or sets the callback used to update the component value + /// + [Parameter] + public EventCallback> ValuesChanged { get; set; } + + /// + /// Handler for when the value of the component has changed + /// + /// the new value + /// an asynchronous operation + private async Task ItemSelected(TItem newValue) + { + this.lastSelectedValue = default; + + if(this.Values.Contains(newValue)) + { + this.Values.Remove(newValue); + } + else + { + this.Values.Add(newValue); + } + + await this.ValuesChanged.InvokeAsync(this.Values); + } + } +} diff --git a/COMET.Web.Common/wwwroot/BookInputConfiguration.json b/COMET.Web.Common/wwwroot/BookInputConfiguration.json new file mode 100644 index 00000000..a0c6a76d --- /dev/null +++ b/COMET.Web.Common/wwwroot/BookInputConfiguration.json @@ -0,0 +1,4 @@ +{ + "ShowName": true, + "ShowShortName" : true +} diff --git a/COMET.Web.Common/wwwroot/css/SVG/icons/close.svg b/COMET.Web.Common/wwwroot/css/SVG/icons/close.svg new file mode 100644 index 00000000..3c597777 --- /dev/null +++ b/COMET.Web.Common/wwwroot/css/SVG/icons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/COMET.Web.Common/wwwroot/css/styles.css b/COMET.Web.Common/wwwroot/css/styles.css index a730d676..d05981b9 100644 --- a/COMET.Web.Common/wwwroot/css/styles.css +++ b/COMET.Web.Common/wwwroot/css/styles.css @@ -60,6 +60,10 @@ background-image: url(SVG/icons/trash.svg) } +.icon-close { + background-image: url(SVG/icons/close.svg) +} + .logo-size { max-width: 95px; height: auto; @@ -500,7 +504,48 @@ color: #16386f; } -.tab-content { +/**MULTICOMBOBOX COMPONENT*/ +::deep.chip { + border: 0; + background-color: #EEEEEE; + border-radius: 15px; + padding: 2.5% 5%; + box-shadow: 0px 0px 0px 1px rgba(0,0,0,0.2); +} + +::deep.chips-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + row-gap: 5px; + column-gap: 5px; +} + +::deep.dxbl-listbox-item { + padding: 0 !important; +} + +::deep.combo-box-item-template { + display: block; + height: 100%; + width: 100%; + padding: 2%; +} + + ::deep.combo-box-item-template.selected { + background-color: #0d6efd; + color: white; + } + +.multi-combo-item-template { + display: flex; + align-items:center; + column-gap: 10px; +} + +/*BOOK EDITOR*/ + +.input-content { display: flex; flex-direction: column; align-items: start; @@ -518,4 +563,4 @@ .editor-row > p { width: 25%; margin: 0; - } \ No newline at end of file + }