From 5556e005afd645f7883e35dd11955828d2e8c30a Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Thu, 11 Apr 2024 13:54:47 +0100 Subject: [PATCH 01/12] implemented drag and drop + move file to folder. TODO - Add suport to move folder to folder - Handle edge cases - Upload/download file - Create/Edit files/folders - Unit tests --- .../FolderFileStructure.razor | 25 ++-- .../FolderFileStructure.razor.cs | 29 +++++ .../FileFolderNodeViewModel.cs | 106 +++++++++++++--- .../FolderFileStructureViewModel.cs | 116 +++++++++++++++++- .../IFolderFileStructureViewModel.cs | 12 +- 5 files changed, 257 insertions(+), 31 deletions(-) diff --git a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor index 1901d9b2..369097f9 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor +++ b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor @@ -28,18 +28,25 @@ Copyright (c) 2023-2024 RHEA System S.A. IconCssClass="@nameof(FileFolderNodeViewModel.IconCssClass)" /> - @nodeContext.Text @{ var row = (FileFolderNodeViewModel)nodeContext.DataItem; +
- if (row.Thing is Folder) - { - - } + @nodeContext.Text + + @if (row.Thing is Folder) + { + + } +
}
diff --git a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs index 75d13f94..4173d912 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs @@ -28,12 +28,16 @@ namespace COMETwebapp.Components.EngineeringModel using CDP4Common.EngineeringModelData; + using COMET.Web.Common.Extensions; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; using DevExpress.Blazor; using Microsoft.AspNetCore.Components; + using ReactiveUI; + /// /// Support class for the /// @@ -55,6 +59,11 @@ public partial class FolderFileStructure /// public Folder SelectedFolder { get; private set; } + /// + /// Gets the dragged node used in drag and drop interactions + /// + public FileFolderNodeViewModel DraggedNode { get; private set; } + /// /// Gets or sets the used to display the folder-file structure /// @@ -92,6 +101,16 @@ public void OnEditFolderClick(FileFolderNodeViewModel row) this.SelectedFolder = this.ViewModel.Folder; } + /// + /// 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.Disposables.Add(this.WhenAnyValue(x => x.ViewModel.IsLoading).SubscribeAsync(_ => this.InvokeAsync(this.StateHasChanged))); + } + /// /// Method invoked after each time the component has rendered interactively and the UI has finished /// updating (for example, after elements have been added to the browser DOM). Any @@ -127,5 +146,15 @@ private void OnClosedFormPopup() this.SelectedFile = null; this.SelectedFolder = null; } + + private void OnDragNode(FileFolderNodeViewModel node) + { + this.DraggedNode = node; + } + + private async Task OnDropNode(FileFolderNodeViewModel targetNode) + { + await this.ViewModel.MoveFile(this.DraggedNode, targetNode); + } } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs index a9bd7b59..31682e4c 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs @@ -27,11 +27,18 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; + using ReactiveUI; + /// /// ViewModel for a node in the File-Folder structure tree /// - public class FileFolderNodeViewModel + public class FileFolderNodeViewModel : ReactiveObject { + /// + /// The backing field for + /// + private string name; + /// /// The list containing the sub content of the current node /// @@ -40,12 +47,16 @@ public class FileFolderNodeViewModel /// /// The thing associated with the model, which can be either a or a /// - public Thing Thing { get; private set; } + public Thing Thing { get; set; } /// /// The name of the current node /// - public string Name { get; private set; } + public string Name + { + get => this.name; + set => this.RaiseAndSetIfChanged(ref this.name, value); + } /// /// The icon css class of the current node, used to display the icon as a html class @@ -55,34 +66,77 @@ public class FileFolderNodeViewModel /// /// Creates a new instance of type /// - /// The file to set the current + /// The thing to set the current , which can be both or /// A collection containing all the node's content - public FileFolderNodeViewModel(File file, IEnumerable content = null) + public FileFolderNodeViewModel(Thing thing, IEnumerable content = null) { - this.Name = file.CurrentFileRevision.Name; - this.IconCssClass = "oi oi-file"; - this.InitializeProperties(file, content); + this.InitializeProperties(thing, content); } /// - /// Creates a new instance of type + /// Creates a new instance of type , as a root node /// - /// The folder to set the current - /// A collection containing all the node's content - public FileFolderNodeViewModel(Folder folder, IEnumerable content = null) + public FileFolderNodeViewModel() { - this.Name = folder.Name; - this.IconCssClass = "oi oi-folder"; - this.InitializeProperties(folder, content); + this.Name = "root"; + this.IconCssClass = "oi oi-target"; } /// - /// Creates a new instance of type , as a root node + /// Updates the contained /// - public FileFolderNodeViewModel() + /// The thing to be set + public void UpdateThing(Thing thing) { - this.Name = "root"; - this.IconCssClass = "oi oi-target"; + if (this.Thing.GetType() != thing.GetType()) + { + throw new InvalidCastException("Cannot change current thing type"); + } + + this.InitializeProperties(thing); + } + + /// + /// Removes a given node from the nested content + /// + /// The node to be removed + public void RemoveChildNode(FileFolderNodeViewModel node) + { + var wasNodeRemoved = this.Content.Remove(node); + + if (wasNodeRemoved) + { + return; + } + + foreach (var folderNodes in this.Content.Where(x => x.Thing is Folder)) + { + folderNodes.RemoveChildNode(node); + } + } + + /// + /// Gets a flat list with all the children nodes + /// + /// The value to set if current node should be included in the result + /// The collection of children + public IEnumerable GetFlatListOfChildrenNodes(bool includeSelf = false) + { + var children = new List(); + + if (includeSelf) + { + children.Add(this); + } + + children.AddRange(this.Content); + + foreach (var content in this.Content) + { + children.AddRange(content.GetFlatListOfChildrenNodes()); + } + + return children; } /// @@ -92,6 +146,20 @@ public FileFolderNodeViewModel() /// The collection of to set the current private void InitializeProperties(Thing thing, IEnumerable content = null) { + switch (thing) + { + case File file: + this.Name = file.CurrentFileRevision.Name; + this.IconCssClass = "oi oi-file"; + break; + case Folder folder: + this.Name = folder.Name; + this.IconCssClass = "oi oi-folder"; + break; + default: + throw new InvalidDataException("The given thing should be either a File or a Folder"); + } + this.Thing = thing; if (content != null) diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs index 8504d417..0c2e9c52 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs @@ -24,15 +24,19 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure { + using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; + using CDP4Dal; + using COMET.Web.Common.Services.SessionManagement; + using COMET.Web.Common.ViewModels.Components.Applications; /// /// View model used to manage the folder file structure /// - public class FolderFileStructureViewModel : IFolderFileStructureViewModel + public class FolderFileStructureViewModel : ApplicationBaseViewModel, IFolderFileStructureViewModel { /// /// Gets or sets the current @@ -48,9 +52,11 @@ public class FolderFileStructureViewModel : IFolderFileStructureViewModel /// Initializes a new instance of the class. /// /// The - public FolderFileStructureViewModel(ISessionService sessionService) + /// The + public FolderFileStructureViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) { this.sessionService = sessionService; + this.InitializeSubscriptions([typeof(File), typeof(Folder)]); } /// @@ -86,10 +92,91 @@ public void InitializeViewModel(FileStore fileStore) { this.CurrentFileStore = fileStore; this.DomainsOfExpertise = this.sessionService.GetSiteDirectory().Domain; + this.CreateStructureTree(); + } + + /// + /// Moves a file to a target folder + /// + /// The file to be moved + /// + /// A + public async Task MoveFile(FileFolderNodeViewModel fileNode, FileFolderNodeViewModel targetFolderNode) + { + this.IsLoading = true; + + var file = (File)fileNode.Thing; + var targetFolder = (Folder)targetFolderNode.Thing; + + var fileClone = file.Clone(true); + var newFileRevision = fileClone.CurrentFileRevision.Clone(true); + + newFileRevision.Iid = Guid.NewGuid(); + newFileRevision.CreatedOn = DateTime.UtcNow; + newFileRevision.ContainingFolder = targetFolder; + + fileClone.FileRevision.Add(newFileRevision); + + var result = await this.sessionService.UpdateThings(this.CurrentFileStore.Clone(true), fileClone, newFileRevision); + await this.sessionService.RefreshSession(); + + this.IsLoading = false; + } + + /// + /// Handles the refresh of the current + /// + /// A + protected override Task OnSessionRefreshed() + { + if (this.AddedThings.Count == 0 && this.UpdatedThings.Count == 0 && this.DeletedThings.Count == 0) + { + return Task.CompletedTask; + } + + this.IsLoading = true; + + var rootNode = this.Structure.First(); + var flatListOfNodes = rootNode.GetFlatListOfChildrenNodes(true).ToList(); + + foreach (var updatedThing in this.UpdatedThings) + { + var nodeToUpdate = flatListOfNodes.First(x => x.Thing?.Iid == updatedThing?.Iid); + var parentNode = GetContainingFolderNodeFromList(flatListOfNodes, updatedThing); + + rootNode.RemoveChildNode(nodeToUpdate); + nodeToUpdate.UpdateThing(updatedThing); + parentNode.Content.Add(nodeToUpdate); + } + + foreach (var nodeToDelete in this.DeletedThings.Select(deletedThing => flatListOfNodes.First(x => x.Thing?.Iid == deletedThing?.Iid))) + { + rootNode.RemoveChildNode(nodeToDelete); + } + + foreach (var addedThing in this.AddedThings) + { + var parentNode = GetContainingFolderNodeFromList(flatListOfNodes, addedThing); + var nodeToAdd = new FileFolderNodeViewModel(addedThing); + parentNode.Content.Add(nodeToAdd); + } + + this.ClearRecordedChanges(); + this.IsLoading = false; + + return Task.CompletedTask; + } + + /// + /// Creates the structure tree, present in + /// + private void CreateStructureTree() + { this.Structure = []; var rootNode = new FileFolderNodeViewModel(); this.LoadFolderContent(rootNode); + this.Structure.Add(rootNode); } @@ -117,5 +204,30 @@ private void LoadFolderContent(FileFolderNodeViewModel folderNode) this.LoadFolderContent(nestedFolder); } } + + /// + /// Gets the containing folder node for a given node, search in a given flat list of nodes + /// + /// The collection of nodes to search for the containing folder node + /// The node for which the containing folder will be searched for + /// The containing folder node + private static FileFolderNodeViewModel GetContainingFolderNodeFromList(IEnumerable listOfNodes, Thing node) + { + FileFolderNodeViewModel parentNode; + + switch (node) + { + case File file: + parentNode = listOfNodes.First(x => x.Thing?.Iid == file.CurrentContainingFolder?.Iid); + break; + case Folder folder: + parentNode = listOfNodes.First(x => x.Thing?.Iid == folder.ContainingFolder?.Iid); + break; + default: + return null; + } + + return parentNode; + } } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs index 082d3964..adf4a80b 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs @@ -27,10 +27,12 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; + using COMET.Web.Common.ViewModels.Components.Applications; + /// /// View model used to manage the folder file structure /// - public interface IFolderFileStructureViewModel + public interface IFolderFileStructureViewModel : IApplicationBaseViewModel { /// /// Initializes the current @@ -62,5 +64,13 @@ public interface IFolderFileStructureViewModel /// Gets or sets the condition to check if the file or folder to be created is locked /// bool IsLocked { get; set; } + + /// + /// Moves a file to a target folder + /// + /// The file to be moved + /// + /// A + Task MoveFile(FileFolderNodeViewModel fileNode, FileFolderNodeViewModel targetFolderNode); } } From 14028dfa99300dd3cd8177b81a91c0d605bb6380 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Thu, 11 Apr 2024 18:07:42 +0100 Subject: [PATCH 02/12] Folder drag and drop + edit folders TODO - Create folder + create/edit file - Handle more edge cases - Upload/download file (this should include file revisions) - Unit tests --- .../EngineeringModel/FileForm.razor.cs | 7 +- .../FolderFileStructure.razor | 4 +- .../FolderFileStructure.razor.cs | 34 +++- .../EngineeringModel/FolderForm.razor.cs | 18 ++- .../Extensions/ServiceCollectionExtensions.cs | 4 + .../FileHandler/FileHandlerViewModel.cs | 145 ++++++++++++++++++ .../FileHandler/IFileHandlerViewModel.cs | 80 ++++++++++ .../FolderFileStructureViewModel.cs | 70 ++------- .../FolderHandler/FolderHandlerViewModel.cs | 136 ++++++++++++++++ .../FolderHandler/IFolderHandlerViewModel.cs | 74 +++++++++ .../IFolderFileStructureViewModel.cs | 30 +--- 11 files changed, 512 insertions(+), 90 deletions(-) create mode 100644 COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/FileHandlerViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs diff --git a/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs index 3aedf626..1fa7eaa1 100644 --- a/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs @@ -26,6 +26,7 @@ namespace COMETwebapp.Components.EngineeringModel { using COMETwebapp.Components.Common; using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; using Microsoft.AspNetCore.Components; @@ -37,13 +38,13 @@ namespace COMETwebapp.Components.EngineeringModel public partial class FileForm : SelectedDataItemForm { /// - /// The for this component + /// The for this component /// [Parameter, Required] - public IFolderFileStructureViewModel ViewModel { get; set; } + public IFileHandlerViewModel ViewModel { get; set; } /// - /// Downloads a the selected file from + /// Downloads a the selected file from /// public void DownloadFile() { diff --git a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor index 369097f9..381e7eab 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor +++ b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor @@ -56,7 +56,7 @@ Copyright (c) 2023-2024 RHEA System S.A. CloseOnOutsideClick="false" Closed="@(this.OnClosedFormPopup)" CssClass="pw-800"> - @@ -65,6 +65,6 @@ Copyright (c) 2023-2024 RHEA System S.A. CloseOnOutsideClick="false" Closed="@(this.OnClosedFormPopup)" CssClass="pw-800"> - diff --git a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs index 4173d912..dc12bb11 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs @@ -82,8 +82,8 @@ public void OnNodeClick(ITreeViewNodeInfo e) return; } - this.ViewModel.File = file; - this.SelectedFile = this.ViewModel.File; + this.ViewModel.FileHandlerViewModel.SelectFile(file); + this.SelectedFile = this.ViewModel.FileHandlerViewModel.File; } /// @@ -97,8 +97,8 @@ public void OnEditFolderClick(FileFolderNodeViewModel row) return; } - this.ViewModel.Folder = folder; - this.SelectedFolder = this.ViewModel.Folder; + this.ViewModel.FolderHandlerViewModel.SelectFolder(folder); + this.SelectedFolder = this.ViewModel.FolderHandlerViewModel.Folder; } /// @@ -147,14 +147,38 @@ private void OnClosedFormPopup() this.SelectedFolder = null; } + /// + /// Method invoked when a node is dragged + /// + /// The dragged node private void OnDragNode(FileFolderNodeViewModel node) { this.DraggedNode = node; } + /// + /// Method invoked when a node is dropped + /// + /// The target node where the has been dropped + /// A private async Task OnDropNode(FileFolderNodeViewModel targetNode) { - await this.ViewModel.MoveFile(this.DraggedNode, targetNode); + if (targetNode.Thing is not Folder and not null) + { + return; + } + + var targetFolder = (Folder)targetNode.Thing; + + switch (this.DraggedNode.Thing) + { + case File file: + await this.ViewModel.FileHandlerViewModel.MoveFile(file, targetFolder); + break; + case Folder folder: + await this.ViewModel.FolderHandlerViewModel.MoveFolder(folder, targetFolder); + break; + } } } } diff --git a/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs index 306ac08c..ca22ee97 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs @@ -25,8 +25,8 @@ namespace COMETwebapp.Components.EngineeringModel { using COMETwebapp.Components.Common; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; - + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; + using Microsoft.AspNetCore.Components; using System.ComponentModel.DataAnnotations; @@ -37,9 +37,19 @@ namespace COMETwebapp.Components.EngineeringModel public partial class FolderForm : SelectedDataItemForm { /// - /// The for this component + /// The for this component /// [Parameter, Required] - public IFolderFileStructureViewModel ViewModel { get; set; } + public IFolderHandlerViewModel ViewModel { get; set; } + + /// + /// Method that is executed when there is a valid submit + /// + /// A + protected override async Task OnValidSubmit() + { + await this.ViewModel.CreateOrEditFolder(this.ShouldCreate); + await base.OnValidSubmit(); + } } } diff --git a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs index ba2ed37d..5614a212 100644 --- a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs +++ b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs @@ -52,6 +52,8 @@ namespace COMETwebapp.Extensions using COMETwebapp.ViewModels.Components.Viewer; using COMETwebapp.ViewModels.Shared.TopMenuEntry; using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; /// /// Extension class for the @@ -112,6 +114,8 @@ public static void RegisterViewModels(this IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/FileHandlerViewModel.cs new file mode 100644 index 00000000..918b5df6 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/FileHandlerViewModel.cs @@ -0,0 +1,145 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + + using COMET.Web.Common.Services.SessionManagement; + using COMET.Web.Common.ViewModels.Components.Applications; + + /// + /// View model used to manage the files in Filestores + /// + public class FileHandlerViewModel : ApplicationBaseViewModel, IFileHandlerViewModel + { + /// + /// Gets or sets the current + /// + private FileStore CurrentFileStore { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The + /// The + public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) + { + this.InitializeSubscriptions([typeof(File), typeof(Folder)]); + } + + /// + /// Gets a collection of the available + /// + public IEnumerable DomainsOfExpertise { get; private set; } + + /// + /// Gets or sets the file to be created/edited + /// + public File File { get; set; } = new(); + + /// + /// Gets or sets the condition to check if the file or folder to be created is locked + /// + public bool IsLocked { get; set; } + + /// + /// Initializes the current + /// + /// The to be set + public void InitializeViewModel(FileStore fileStore) + { + this.CurrentFileStore = fileStore; + this.DomainsOfExpertise = this.SessionService.GetSiteDirectory().Domain; + } + + /// + /// Selects the current + /// + /// The file to be set + public void SelectFile(File file) + { + this.File = file.Clone(true); + this.IsLocked = this.File.LockedBy is not null; + } + + /// + /// Moves a file to a target folder + /// + /// The file to be moved + /// The target folder + /// A + public async Task MoveFile(File file, Folder targetFolder) + { + this.IsLoading = true; + + var fileClone = file.Clone(true); + var newFileRevision = fileClone.CurrentFileRevision.Clone(true); + + newFileRevision.Iid = Guid.NewGuid(); + newFileRevision.CreatedOn = DateTime.UtcNow; + newFileRevision.ContainingFolder = targetFolder; + + fileClone.FileRevision.Add(newFileRevision); + + await this.SessionService.UpdateThings(this.CurrentFileStore.Clone(true), fileClone, newFileRevision); + await this.SessionService.RefreshSession(); + + this.IsLoading = false; + } + + /// + /// Creates a file into a target folder + /// + /// The target folder + /// The local file path for the file revision + /// A + public async Task CreateFile(Folder targetFolder, string localFilePath) + { + this.IsLoading = true; + + var fileRevision = new FileRevision() + { + LocalPath = localFilePath, + ContainingFolder = targetFolder + }; + + this.File.FileRevision.Add(fileRevision); + + await this.SessionService.UpdateThings(this.CurrentFileStore.Clone(true), this.File, fileRevision); + await this.SessionService.RefreshSession(); + + this.IsLoading = false; + } + + /// + /// Handles the refresh of the current + /// + /// A + protected override Task OnSessionRefreshed() => Task.CompletedTask; + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs new file mode 100644 index 00000000..5a4d6f59 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs @@ -0,0 +1,80 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using COMET.Web.Common.ViewModels.Components.Applications; + + /// + /// View model used to manage the files in Filestores + /// + public interface IFileHandlerViewModel : IApplicationBaseViewModel + { + /// + /// Initializes the current + /// + /// The to be set + void InitializeViewModel(FileStore fileStore); + + /// + /// Gets or sets the file to be created/edited + /// + File File { get; set; } + + /// + /// Gets a collection of the available + /// + IEnumerable DomainsOfExpertise { get; } + + /// + /// Gets or sets the condition to check if the file or folder to be created is locked + /// + bool IsLocked { get; set; } + + /// + /// Moves a file to a target folder + /// + /// The file to be moved + /// The target folder + /// A + Task MoveFile(File file, Folder targetFolder); + + /// + /// Creates a file into a target folder + /// + /// The target folder + /// The local file path for the file revision + /// A + Task CreateFile(Folder targetFolder, string localFilePath); + + /// + /// Selects the current + /// + /// The file to be set + void SelectFile(File file); + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs index 0c2e9c52..ab1819e8 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs @@ -26,13 +26,15 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure { using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; - using CDP4Common.SiteDirectoryData; using CDP4Dal; using COMET.Web.Common.Services.SessionManagement; using COMET.Web.Common.ViewModels.Components.Applications; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; + /// /// View model used to manage the folder file structure /// @@ -43,46 +45,35 @@ public class FolderFileStructureViewModel : ApplicationBaseViewModel, IFolderFil /// private FileStore CurrentFileStore { get; set; } - /// - /// Gets or sets the - /// - private readonly ISessionService sessionService; - /// /// Initializes a new instance of the class. /// /// The /// The - public FolderFileStructureViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) + /// The + /// The + public FolderFileStructureViewModel(ISessionService sessionService, ICDPMessageBus messageBus, IFileHandlerViewModel fileHandlerViewModel, IFolderHandlerViewModel folderHandlerViewModel) + : base(sessionService, messageBus) { - this.sessionService = sessionService; + this.FileHandlerViewModel = fileHandlerViewModel; + this.FolderHandlerViewModel = folderHandlerViewModel; this.InitializeSubscriptions([typeof(File), typeof(Folder)]); } /// - /// Gets a collection of the available - /// - public IEnumerable DomainsOfExpertise { get; private set; } - - /// - /// The folder-file hierarchically structured + /// Gets the /// - public List Structure { get; set; } = []; + public IFileHandlerViewModel FileHandlerViewModel { get; private set; } /// - /// Gets or sets the file to be created/edited + /// Gets the /// - public File File { get; set; } = new(); + public IFolderHandlerViewModel FolderHandlerViewModel { get; private set; } /// - /// Gets or sets the folder to be created/edited - /// - public Folder Folder { get; set; } = new(); - - /// - /// Gets or sets the condition to check if the file or folder to be created is locked + /// The folder-file hierarchically structured /// - public bool IsLocked { get; set; } + public List Structure { get; set; } = []; /// /// Initializes the current @@ -91,38 +82,11 @@ public FolderFileStructureViewModel(ISessionService sessionService, ICDPMessageB public void InitializeViewModel(FileStore fileStore) { this.CurrentFileStore = fileStore; - this.DomainsOfExpertise = this.sessionService.GetSiteDirectory().Domain; + this.FileHandlerViewModel.InitializeViewModel(this.CurrentFileStore); + this.FolderHandlerViewModel.InitializeViewModel(this.CurrentFileStore); this.CreateStructureTree(); } - /// - /// Moves a file to a target folder - /// - /// The file to be moved - /// - /// A - public async Task MoveFile(FileFolderNodeViewModel fileNode, FileFolderNodeViewModel targetFolderNode) - { - this.IsLoading = true; - - var file = (File)fileNode.Thing; - var targetFolder = (Folder)targetFolderNode.Thing; - - var fileClone = file.Clone(true); - var newFileRevision = fileClone.CurrentFileRevision.Clone(true); - - newFileRevision.Iid = Guid.NewGuid(); - newFileRevision.CreatedOn = DateTime.UtcNow; - newFileRevision.ContainingFolder = targetFolder; - - fileClone.FileRevision.Add(newFileRevision); - - var result = await this.sessionService.UpdateThings(this.CurrentFileStore.Clone(true), fileClone, newFileRevision); - await this.sessionService.RefreshSession(); - - this.IsLoading = false; - } - /// /// Handles the refresh of the current /// diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs new file mode 100644 index 00000000..97923dc9 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs @@ -0,0 +1,136 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler +{ + using CDP4Common.CommonData; + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + + using COMET.Web.Common.Services.SessionManagement; + using COMET.Web.Common.ViewModels.Components.Applications; + + /// + /// View model used to manage the folders in Filestores + /// + public class FolderHandlerViewModel : ApplicationBaseViewModel, IFolderHandlerViewModel + { + /// + /// Gets or sets the current + /// + private FileStore CurrentFileStore { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The + /// The + public FolderHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) + { + this.InitializeSubscriptions([typeof(File), typeof(Folder)]); + } + + /// + /// Gets a collection of the available + /// + public IEnumerable DomainsOfExpertise { get; private set; } + + /// + /// Gets or sets the folder to be created/edited + /// + public Folder Folder { get; set; } = new(); + + /// + /// Initializes the current + /// + /// The to be set + public void InitializeViewModel(FileStore fileStore) + { + this.CurrentFileStore = fileStore; + this.DomainsOfExpertise = this.SessionService.GetSiteDirectory().Domain; + } + + /// + /// Selects the current + /// + /// The folder to be set + public void SelectFolder(Folder folder) + { + this.Folder = folder.Clone(true); + } + + /// + /// Moves a folder to a target folder + /// + /// The folder to be moved + /// the target folders + /// A + public async Task MoveFolder(Folder folder, Folder targetFolder) + { + this.IsLoading = true; + + var folderClone = folder.Clone(true); + folderClone.ContainingFolder = targetFolder; + + await this.SessionService.UpdateThings(this.CurrentFileStore.Clone(true), folderClone); + await this.SessionService.RefreshSession(); + + this.IsLoading = false; + } + + /// + /// Creates or edits a folder + /// + /// the value to check if the should be created or edited + /// A + public async Task CreateOrEditFolder(bool shouldCreate) + { + this.IsLoading = true; + + var thingsToCreate = new List(); + var fileStoreClone = this.CurrentFileStore.Clone(true); + + if (shouldCreate) + { + fileStoreClone.Folder.Add(this.Folder); + thingsToCreate.Add(fileStoreClone); + } + + thingsToCreate.Add(this.Folder); + + await this.SessionService.UpdateThings(this.CurrentFileStore.Clone(true), thingsToCreate); + await this.SessionService.RefreshSession(); + + this.IsLoading = false; + } + + /// + /// Handles the refresh of the current + /// + /// A + protected override Task OnSessionRefreshed() => Task.CompletedTask; + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs new file mode 100644 index 00000000..1819e435 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs @@ -0,0 +1,74 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using COMET.Web.Common.ViewModels.Components.Applications; + + /// + /// View model used to manage the folders in Filestores + /// + public interface IFolderHandlerViewModel : IApplicationBaseViewModel + { + /// + /// Initializes the current + /// + /// The to be set + void InitializeViewModel(FileStore fileStore); + + /// + /// Gets a collection of the available + /// + IEnumerable DomainsOfExpertise { get; } + + /// + /// Gets or sets the folder to be created/edited + /// + Folder Folder { get; set; } + + /// + /// Selects the current + /// + /// The folder to be set + public void SelectFolder(Folder folder); + + /// + /// Creates or edits a folder + /// + /// the value to check if the should be created or edited + /// A + Task CreateOrEditFolder(bool shouldCreate); + + /// + /// Moves a folder to a target folder + /// + /// The folder to be moved + /// the target folders + /// A + Task MoveFolder(Folder folder, Folder targetFolder); + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs index adf4a80b..9a2d30d4 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs @@ -25,10 +25,12 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure { using CDP4Common.EngineeringModelData; - using CDP4Common.SiteDirectoryData; using COMET.Web.Common.ViewModels.Components.Applications; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; + /// /// View model used to manage the folder file structure /// @@ -46,31 +48,13 @@ public interface IFolderFileStructureViewModel : IApplicationBaseViewModel List Structure { get; set; } /// - /// Gets or sets the file to be created/edited - /// - File File { get; set; } - - /// - /// Gets or sets the folder to be created/edited - /// - Folder Folder { get; set; } - - /// - /// Gets a collection of the available - /// - IEnumerable DomainsOfExpertise { get; } - - /// - /// Gets or sets the condition to check if the file or folder to be created is locked + /// Gets the /// - bool IsLocked { get; set; } + IFileHandlerViewModel FileHandlerViewModel { get; } /// - /// Moves a file to a target folder + /// Gets the /// - /// The file to be moved - /// - /// A - Task MoveFile(FileFolderNodeViewModel fileNode, FileFolderNodeViewModel targetFolderNode); + IFolderHandlerViewModel FolderHandlerViewModel { get; } } } From 9abf46276e72c611cf359e2b10936b23eda7b78a Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Fri, 12 Apr 2024 17:28:25 +0100 Subject: [PATCH 03/12] download and forms working TODO - Improve file revision form (add support to multiple filetypes) - Add validation - Improve design for revisions table - Refactor code - Unit tests --- .../EngineeringModel/FileForm.razor | 46 ++++- .../EngineeringModel/FileForm.razor.cs | 25 ++- .../EngineeringModel/FileRevisionsTable.razor | 67 +++++++ .../FileRevisionsTable.razor.cs | 183 ++++++++++++++++++ .../FolderFileStructure.razor | 26 ++- .../FolderFileStructure.razor.cs | 48 +++-- .../EngineeringModel/FolderForm.razor | 39 +++- .../EngineeringModel/FolderForm.razor.cs | 18 ++ COMETwebapp/Extensions/FileExtensions.cs | 50 +++++ COMETwebapp/Extensions/FolderExtensions.cs | 45 +++++ .../Extensions/ServiceCollectionExtensions.cs | 1 + COMETwebapp/Pages/_Host.cshtml | 1 + .../Interoperability/IJsUtilitiesService.cs | 40 ++++ .../Interoperability/JsUtilitiesService.cs | 58 ++++++ .../FileFolderNodeViewModel.cs | 2 +- .../FileHandler/FileHandlerViewModel.cs | 150 +++++++++++++- .../FileHandler/IFileHandlerViewModel.cs | 38 +++- .../FolderHandler/FolderHandlerViewModel.cs | 28 ++- .../FolderHandler/IFolderHandlerViewModel.cs | 11 ++ .../Rows/FileRevisionRowViewModel.cs | 97 ++++++++++ COMETwebapp/wwwroot/Scripts/utilities.js | 21 ++ 21 files changed, 945 insertions(+), 49 deletions(-) create mode 100644 COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor create mode 100644 COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor.cs create mode 100644 COMETwebapp/Extensions/FileExtensions.cs create mode 100644 COMETwebapp/Extensions/FolderExtensions.cs create mode 100644 COMETwebapp/Services/Interoperability/IJsUtilitiesService.cs create mode 100644 COMETwebapp/Services/Interoperability/JsUtilitiesService.cs create mode 100644 COMETwebapp/ViewModels/Components/EngineeringModel/Rows/FileRevisionRowViewModel.cs create mode 100644 COMETwebapp/wwwroot/Scripts/utilities.js diff --git a/COMETwebapp/Components/EngineeringModel/FileForm.razor b/COMETwebapp/Components/EngineeringModel/FileForm.razor index e310b44b..20861ef6 100644 --- a/COMETwebapp/Components/EngineeringModel/FileForm.razor +++ b/COMETwebapp/Components/EngineeringModel/FileForm.razor @@ -14,6 +14,7 @@ Copyright (c) 2023-2024 RHEA System S.A. You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/. -------------------------------------------------------------------------------> +@using COMETwebapp.Extensions @inherits SelectedDataItemForm @@ -29,15 +30,42 @@ Copyright (c) 2023-2024 RHEA System S.A. @bind-Value="@this.ViewModel.File.Owner" CssClass="cw-480" /> + + @if (this.ShouldCreate) + { + + + + @itemTemplateContext.GetFolderPath() + + + + } + + + + +
- - Download + + Delete - - + Save @@ -47,4 +75,12 @@ Copyright (c) 2023-2024 RHEA System S.A. Cancel
-
\ No newline at end of file + + + + You are about to delete the File: @(this.ViewModel.File.CurrentFileRevision?.Name) +
+ + +
+
diff --git a/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs index 1fa7eaa1..ff1114b9 100644 --- a/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs @@ -25,7 +25,6 @@ namespace COMETwebapp.Components.EngineeringModel { using COMETwebapp.Components.Common; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; using Microsoft.AspNetCore.Components; @@ -44,11 +43,29 @@ public partial class FileForm : SelectedDataItemForm public IFileHandlerViewModel ViewModel { get; set; } /// - /// Downloads a the selected file from + /// Gets or sets the value to check if the deletion popup is visible /// - public void DownloadFile() + private bool IsDeletePopupVisible { get; set; } + + /// + /// Method that is executed when there is a valid submit + /// + /// A + protected override async Task OnValidSubmit() + { + await this.ViewModel.CreateOrEditFile(this.ShouldCreate); + await base.OnValidSubmit(); + } + + /// + /// Method that is executed when a deletion is done + /// + /// A + private async Task OnDelete() { - // downloads a file => to be done + this.IsDeletePopupVisible = false; + await this.ViewModel.DeleteFile(); + await base.OnCancel(); } } } diff --git a/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor b/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor new file mode 100644 index 00000000..6a947a13 --- /dev/null +++ b/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor @@ -0,0 +1,67 @@ + +@using COMETwebapp.ViewModels.Components.ReferenceData.Rows +@inherits DisposableComponent + + + + + + + + + + + + @{ + var row = (FileRevisionRowViewModel)context.DataItem; + + + + } + + + + + + + + + $".{x.Extension}")))"> + + + + + + + + +
+ +
+ +
diff --git a/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor.cs b/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor.cs new file mode 100644 index 00000000..8a1545f5 --- /dev/null +++ b/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor.cs @@ -0,0 +1,183 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Components.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using COMET.Web.Common.Components; + using COMET.Web.Common.Services.SessionManagement; + + using COMETwebapp.Extensions; + using COMETwebapp.ViewModels.Components.EngineeringModel.Rows; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components; + using Microsoft.AspNetCore.Components.Forms; + + /// + /// Support class for the + /// + public partial class FileRevisionsTable : DisposableComponent + { + /// + /// The file to be handled + /// + [Parameter] + public File File { get; set; } + + /// + /// A collection of file revisions to display for selection + /// + [Parameter] + public IEnumerable FileRevisions { get; set; } + + /// + /// A collection of the acceptable file extensions + /// + [Parameter] + public IEnumerable AcceptableFileExtensions { get; set; } + + /// + /// Method used to download a file revision file + /// + [Parameter] + public Action DownloadFileRevision { get; set; } + + /// + /// The method that is executed when the file revisions change + /// + [Parameter] + public EventCallback> FileRevisionsChanged { get; set; } + + /// + /// Gets or sets the grid control that is being customized. + /// + private IGrid Grid { get; set; } + + /// + /// The file revision that will be handled for both edit and add forms + /// + private FileRevision FileRevision { get; set; } = new(); + + /// + /// The directory where uploaded files are stored + /// + private const string UploadsDirectory = "wwwroot/uploads/"; + + /// + /// The maximum file size to upload in megabytes + /// + private const double MaxUploadFileSizeInMb = 512; + + /// + /// Gets or sets the uploaded file path + /// + private string UploadedFilePath { get; set; } + + /// + /// Gets or sets the error message that is displayed in the component + /// + public string ErrorMessage { get; private set; } + + /// + /// Method that is invoked when the edit/add file revision form is being saved + /// + private async Task OnEditFileRevisionSaving() + { + var listOfFileRevisions = this.FileRevisions.ToList(); + listOfFileRevisions.Add(this.FileRevision); + + this.FileRevisions = listOfFileRevisions; + await this.FileRevisionsChanged.InvokeAsync(this.FileRevisions); + } + + /// + /// Method that is invoked when a file revision row is being removed + /// + private void RemoveFileRevision(FileRevisionRowViewModel row) + { + this.File.FileRevision.Remove(row.Thing); + } + + /// + /// Method invoked when creating a new file revision + /// + /// A + private void CustomizeEditFileRevision(GridCustomizeEditModelEventArgs e) + { + var dataItem = (FileRevisionRowViewModel)e.DataItem; + this.FileRevision = dataItem == null ? new FileRevision() : dataItem.Thing; + this.FileRevision.ContainingFolder = this.File.CurrentContainingFolder; + e.EditModel = this.FileRevision; + } + + /// + /// Method used to retrieve the available rows, given the from + /// + /// A collection of s to display + private List GetRows() + { + return this.FileRevisions.Select(x => new FileRevisionRowViewModel(x)).ToList(); + } + + /// + /// Method that is invoked when a file is uploaded to server + /// + private async Task OnFileUpload(InputFileChangeEventArgs e) + { + var maxUploadFileSizeInBytes = (long)(MaxUploadFileSizeInMb * 1024 * 1024); + + if (e.File.Size > maxUploadFileSizeInBytes) + { + this.ErrorMessage = $"The max file size is {MaxUploadFileSizeInMb} MB"; + return; + } + + this.UploadedFilePath = Path.Combine(UploadsDirectory, Guid.NewGuid().ToString()); + Directory.CreateDirectory(UploadsDirectory); + + await using (var fileStream = new FileStream(this.UploadedFilePath, FileMode.Create)) + { + await e.File.OpenReadStream(maxUploadFileSizeInBytes).CopyToAsync(fileStream); + } + + this.FileRevision.Name = e.File.Name; + this.FileRevision.LocalPath = this.UploadedFilePath; + + await this.InvokeAsync(this.StateHasChanged); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Value asserting if this component should dispose or not + protected override void Dispose(bool disposing) + { + FileExtensions.TryDelete(this.UploadedFilePath); + } + } +} diff --git a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor index 381e7eab..d20ffe1e 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor +++ b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor @@ -17,6 +17,16 @@ Copyright (c) 2023-2024 RHEA System S.A. @using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure @inherits DisposableComponent + + Create Folder + + + + Create File + + - + Width="40%"> + OnCanceled="@(this.OnClosedFormPopup)" + OnSaved="@(this.OnClosedFormPopup)" + ShouldCreate="@(this.ShouldCreateFile)" /> - + Width="40%"> + OnCanceled="@(this.OnClosedFormPopup)" + OnSaved="@(this.OnClosedFormPopup)" + ShouldCreate="@(this.ShouldCreateFolder)"/> diff --git a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs index dc12bb11..3e7e541c 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor.cs @@ -50,14 +50,24 @@ public partial class FolderFileStructure public IFolderFileStructureViewModel ViewModel { get; set; } /// - /// Gets the selected file from the + /// Gets the condition to verify if a file should be created /// - public File SelectedFile { get; private set; } + public bool ShouldCreateFile { get; private set; } /// - /// Gets the selected folder from the + /// Gets the condition to check the visibility of the file form /// - public Folder SelectedFolder { get; private set; } + public bool IsFileFormVisibile { get; private set; } + + /// + /// Gets the condition to verify if a folder should be created + /// + public bool ShouldCreateFolder { get; private set; } + + /// + /// Gets the condition to check the visibility of the folder form + /// + public bool IsFolderFormVisibile { get; private set; } /// /// Gets the dragged node used in drag and drop interactions @@ -76,29 +86,39 @@ public partial class FolderFileStructure public void OnNodeClick(ITreeViewNodeInfo e) { var dataItem = (FileFolderNodeViewModel)e.DataItem; + this.OnEditFileClick(dataItem); + } - if (dataItem.Thing is not File file) + /// + /// Method that is triggered every time the edit folder is clicked + /// + /// The selected row + public void OnEditFolderClick(FileFolderNodeViewModel row = null) + { + if (row is { Thing: not Folder }) { return; } - this.ViewModel.FileHandlerViewModel.SelectFile(file); - this.SelectedFile = this.ViewModel.FileHandlerViewModel.File; + this.ViewModel.FolderHandlerViewModel.SelectFolder(row == null ? new Folder() : (Folder)row.Thing); + this.ShouldCreateFolder = row == null; + this.IsFolderFormVisibile = true; } /// - /// Method that is triggered every time the edit folder icon is clicked + /// Method that is triggered every time the edit file is clicked /// /// The selected row - public void OnEditFolderClick(FileFolderNodeViewModel row) + public void OnEditFileClick(FileFolderNodeViewModel row = null) { - if (row.Thing is not Folder folder) + if (row is { Thing: not File }) { return; } - this.ViewModel.FolderHandlerViewModel.SelectFolder(folder); - this.SelectedFolder = this.ViewModel.FolderHandlerViewModel.Folder; + this.ViewModel.FileHandlerViewModel.SelectFile(row == null ? new File() : (File)row.Thing); + this.ShouldCreateFile = row == null; + this.IsFileFormVisibile = true; } /// @@ -143,8 +163,8 @@ protected override void OnAfterRender(bool firstRender) /// private void OnClosedFormPopup() { - this.SelectedFile = null; - this.SelectedFolder = null; + this.IsFileFormVisibile = false; + this.IsFolderFormVisibile = false; } /// diff --git a/COMETwebapp/Components/EngineeringModel/FolderForm.razor b/COMETwebapp/Components/EngineeringModel/FolderForm.razor index 0adcc479..c2b06cfa 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderForm.razor +++ b/COMETwebapp/Components/EngineeringModel/FolderForm.razor @@ -14,28 +14,51 @@ Copyright (c) 2023-2024 RHEA System S.A. You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/. -------------------------------------------------------------------------------> +@using COMETwebapp.Extensions @inherits SelectedDataItemForm - - + + + CssClass="cw-480"/> + + @if (this.ShouldCreate) + { + + + + @itemTemplateContext.GetFolderPath() + + + + } +
+ + Delete + + Save - + @@ -43,3 +66,11 @@ Copyright (c) 2023-2024 RHEA System S.A.
+ + + You are about to delete the Folder: @(this.ViewModel.Folder.Name) +
+ + +
+
diff --git a/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs index ca22ee97..1822be8f 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs @@ -31,6 +31,8 @@ namespace COMETwebapp.Components.EngineeringModel using System.ComponentModel.DataAnnotations; + using CDP4Common.EngineeringModelData; + /// /// Support class for the /// @@ -42,6 +44,11 @@ public partial class FolderForm : SelectedDataItemForm [Parameter, Required] public IFolderHandlerViewModel ViewModel { get; set; } + /// + /// Gets or sets the value to check if the deletion popup is visible + /// + private bool IsDeletePopupVisible { get; set; } + /// /// Method that is executed when there is a valid submit /// @@ -51,5 +58,16 @@ protected override async Task OnValidSubmit() await this.ViewModel.CreateOrEditFolder(this.ShouldCreate); await base.OnValidSubmit(); } + + /// + /// Method that is executed when a deletion is done + /// + /// A + private async Task OnDelete() + { + this.IsDeletePopupVisible = false; + await this.ViewModel.DeleteFolder(); + await base.OnCancel(); + } } } diff --git a/COMETwebapp/Extensions/FileExtensions.cs b/COMETwebapp/Extensions/FileExtensions.cs new file mode 100644 index 00000000..59e55218 --- /dev/null +++ b/COMETwebapp/Extensions/FileExtensions.cs @@ -0,0 +1,50 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Extensions +{ + /// + /// Extension class for File + /// + public static class FileExtensions + { + /// + /// Tries to delete a file in the given file path + /// + /// The file path + /// true if the file was deleted, otherwise false + public static bool TryDelete(string filePath) + { + try + { + File.Delete(filePath); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/COMETwebapp/Extensions/FolderExtensions.cs b/COMETwebapp/Extensions/FolderExtensions.cs new file mode 100644 index 00000000..2bfcf83f --- /dev/null +++ b/COMETwebapp/Extensions/FolderExtensions.cs @@ -0,0 +1,45 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Extensions +{ + using CDP4Common.EngineeringModelData; + + /// + /// Extension class for Folder + /// + public static class FolderExtensions + { + /// + /// Gets the given folder path, including the folder itself + /// + /// The folder to get the path + /// The path + public static string GetFolderPath(this Folder folder) + { + var path = $"{folder.Path}/{folder.Name}/"; + return path; + } + } +} diff --git a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs index 5614a212..c4262818 100644 --- a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs +++ b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs @@ -73,6 +73,7 @@ public static void RegisterServices(this IServiceCollection serviceCollection) serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); serviceCollection.AddHttpClient(); serviceCollection.AddAntDesign(); } diff --git a/COMETwebapp/Pages/_Host.cshtml b/COMETwebapp/Pages/_Host.cshtml index d6cba627..d13aba18 100644 --- a/COMETwebapp/Pages/_Host.cshtml +++ b/COMETwebapp/Pages/_Host.cshtml @@ -102,5 +102,6 @@ + \ No newline at end of file diff --git a/COMETwebapp/Services/Interoperability/IJsUtilitiesService.cs b/COMETwebapp/Services/Interoperability/IJsUtilitiesService.cs new file mode 100644 index 00000000..56ffcc62 --- /dev/null +++ b/COMETwebapp/Services/Interoperability/IJsUtilitiesService.cs @@ -0,0 +1,40 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Services.Interoperability +{ + /// + /// The service used to provide js utilities + /// + public interface IJsUtilitiesService + { + /// + /// Downloads a file from a stream asynchronously + /// + /// the stream + /// the file name + /// an asynchronous operation + Task DownloadFileFromStreamAsync(Stream stream, string fileName); + } +} diff --git a/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs b/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs new file mode 100644 index 00000000..93c2e127 --- /dev/null +++ b/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs @@ -0,0 +1,58 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Services.Interoperability +{ + using Microsoft.JSInterop; + + /// + /// The service used to provide js utilities + /// + public class JsUtilitiesService : InteroperabilityService, IJsUtilitiesService + { + /// + /// Creates a new instance of + /// + /// the + public JsUtilitiesService(IJSRuntime jsRuntime) : base(jsRuntime) + { + } + + /// + /// Downloads a file from a stream asynchronously + /// + /// the stream + /// the file name + /// an asynchronous operation + public async Task DownloadFileFromStreamAsync(Stream stream, string fileName) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(fileName); + + using var streamRef = new DotNetStreamReference(stream: stream); + + await this.JsRuntime.InvokeVoidAsync("DownloadFileFromStream", fileName, streamRef); + } + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs index 31682e4c..94c9dcd9 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs @@ -149,7 +149,7 @@ private void InitializeProperties(Thing thing, IEnumerable /// View model used to manage the files in Filestores ///
@@ -42,26 +47,60 @@ public class FileHandlerViewModel : ApplicationBaseViewModel, IFileHandlerViewMo ///
private FileStore CurrentFileStore { get; set; } + /// + /// Gets the + /// + private readonly ILogger logger; + /// /// Initializes a new instance of the class. /// /// The /// The - public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) + /// The + public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus, ILogger logger, IJsUtilitiesService jsUtilitiesService) + : base(sessionService, messageBus) { + this.JsUtilitiesService = jsUtilitiesService; + this.logger = logger; this.InitializeSubscriptions([typeof(File), typeof(Folder)]); } + /// + /// Gets or sets the + /// + public IJsUtilitiesService JsUtilitiesService { get; private set; } + /// /// Gets a collection of the available /// public IEnumerable DomainsOfExpertise { get; private set; } + /// + /// Gets a collection of the available + /// + public IEnumerable FileTypes { get; private set; } + + /// + /// Gets a collection of the available s + /// + public IEnumerable Folders { get; private set; } + /// /// Gets or sets the file to be created/edited /// public File File { get; set; } = new(); + /// + /// Gets or sets a collection of the file revisions to be created/edited + /// + public IEnumerable FileRevisions { get; set; } + + /// + /// Gets or sets the selected folder to create a file revision + /// + public Folder SelectedFolder { get; set; } + /// /// Gets or sets the condition to check if the file or folder to be created is locked /// @@ -75,6 +114,11 @@ public void InitializeViewModel(FileStore fileStore) { this.CurrentFileStore = fileStore; this.DomainsOfExpertise = this.SessionService.GetSiteDirectory().Domain; + this.FileTypes = this.SessionService.GetSiteDirectory().AvailableReferenceDataLibraries().SelectMany(x => x.FileType); + + var folders = this.CurrentFileStore.Folder.ToList(); + folders.Add(null); + this.Folders = folders; } /// @@ -85,6 +129,7 @@ public void SelectFile(File file) { this.File = file.Clone(true); this.IsLocked = this.File.LockedBy is not null; + this.FileRevisions = this.File.FileRevision; } /// @@ -112,34 +157,119 @@ public async Task MoveFile(File file, Folder targetFolder) this.IsLoading = false; } + public void SetupFileWithNewFileRevisions() + { + + } + /// - /// Creates a file into a target folder + /// Creates or edits a file /// - /// The target folder - /// The local file path for the file revision + /// The condition to check if the file should be created /// A - public async Task CreateFile(Folder targetFolder, string localFilePath) + public async Task CreateOrEditFile(bool shouldCreate) { this.IsLoading = true; - var fileRevision = new FileRevision() + var thingsToCreate = new List(); + var fileStoreClone = this.CurrentFileStore.Clone(false); + var engineeringModel = this.CurrentFileStore.GetContainerOfType(); + + this.File.LockedBy = this.IsLocked switch { - LocalPath = localFilePath, - ContainingFolder = targetFolder + true when this.File.LockedBy == null => this.SessionService.Session.ActivePerson, + false when this.File.LockedBy != null => null, + _ => this.File.LockedBy }; - this.File.FileRevision.Add(fileRevision); + var newFileRevisions = this.FileRevisions.Where(x => !this.File.FileRevision.Contains(x)); + + foreach (var fileRevision in newFileRevisions) + { + var fileExtension = Path.GetExtension(fileRevision.Name); + var fileType = engineeringModel.RequiredRdls.SelectMany(x => x.FileType).First(x => $".{x.Extension}" == fileExtension); + + fileRevision.FileType.Add(fileType); + fileRevision.Name = Path.GetFileNameWithoutExtension(fileRevision.Name); + fileRevision.Creator = engineeringModel.GetActiveParticipant(this.SessionService.Session.ActivePerson); + fileRevision.CreatedOn = DateTime.UtcNow; + fileRevision.ContentHash = CalculateContentHash(fileRevision.LocalPath); - await this.SessionService.UpdateThings(this.CurrentFileStore.Clone(true), this.File, fileRevision); + this.File.FileRevision.Add(fileRevision); + thingsToCreate.Add(fileRevision); + } + + if (shouldCreate) + { + fileStoreClone.File.Add(this.File); + thingsToCreate.Add(fileStoreClone); + } + + thingsToCreate.Add(this.File); + + await this.SessionService.UpdateThings(fileStoreClone, thingsToCreate); await this.SessionService.RefreshSession(); + // delete all stored files + this.IsLoading = false; } + /// + /// Deletes the current file + /// + /// A + public async Task DeleteFile() + { + var clonedContainer = this.File.Container.Clone(false); + + await this.SessionService.DeleteThing(clonedContainer, this.File); + await this.SessionService.RefreshSession(); + } + + /// + /// Downloads a file revision + /// + /// the file revision + /// A + public async Task DownloadFileRevision(FileRevision fileRevision) + { + this.logger.LogInformation("Starting File Revision download..."); + var fileRevisionNameWithExtension = $"{fileRevision.Name}.{fileRevision.FileType.First().Extension}"; + + try + { + var bytes = await this.SessionService.Session.ReadFile(fileRevision); + var stream = new MemoryStream(bytes); + await this.JsUtilitiesService.DownloadFileFromStreamAsync(stream, fileRevisionNameWithExtension); + this.logger.LogInformation("Downloading PDF..."); + } + catch (Exception ex) + { + this.logger.LogError(ex,"File Revision could not be downloaded") ; + } + } + /// /// Handles the refresh of the current /// /// A protected override Task OnSessionRefreshed() => Task.CompletedTask; + + /// + /// Calculates the hash of the file's content + /// + /// the path to the file + /// the hash of the content + private static string CalculateContentHash(string filePath) + { + if (filePath == null) + { + return null; + } + + using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + return StreamToHashComputer.CalculateSha1HashFromStream(fileStream); + } } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs index 5a4d6f59..02e2c0e4 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs @@ -55,6 +55,26 @@ public interface IFileHandlerViewModel : IApplicationBaseViewModel /// bool IsLocked { get; set; } + /// + /// Gets a collection of the available s + /// + IEnumerable Folders { get; } + + /// + /// Gets or sets the selected folder to create a file revision + /// + Folder SelectedFolder { get; set; } + + /// + /// Gets or sets a collection of the file revisions to be created/edited + /// + IEnumerable FileRevisions { get; set; } + + /// + /// Gets a collection of the available + /// + IEnumerable FileTypes { get; } + /// /// Moves a file to a target folder /// @@ -66,15 +86,27 @@ public interface IFileHandlerViewModel : IApplicationBaseViewModel /// /// Creates a file into a target folder /// - /// The target folder - /// The local file path for the file revision + /// /// A - Task CreateFile(Folder targetFolder, string localFilePath); + Task CreateOrEditFile(bool shouldCreate); /// /// Selects the current /// /// The file to be set void SelectFile(File file); + + /// + /// Deletes the current file + /// + /// A + Task DeleteFile(); + + /// + /// Downloads a file revision + /// + /// the file revision + /// A + Task DownloadFileRevision(FileRevision fileRevision); } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs index 97923dc9..562cc4a5 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs @@ -58,6 +58,11 @@ public FolderHandlerViewModel(ISessionService sessionService, ICDPMessageBus mes /// public IEnumerable DomainsOfExpertise { get; private set; } + /// + /// Gets a collection of the available s + /// + public IEnumerable Folders { get; private set; } + /// /// Gets or sets the folder to be created/edited /// @@ -71,6 +76,10 @@ public void InitializeViewModel(FileStore fileStore) { this.CurrentFileStore = fileStore; this.DomainsOfExpertise = this.SessionService.GetSiteDirectory().Domain; + + var folders = this.CurrentFileStore.Folder.ToList(); + folders.Add(null); + this.Folders = folders; } /// @@ -111,22 +120,37 @@ public async Task CreateOrEditFolder(bool shouldCreate) this.IsLoading = true; var thingsToCreate = new List(); - var fileStoreClone = this.CurrentFileStore.Clone(true); + var fileStoreClone = this.CurrentFileStore.Clone(false); if (shouldCreate) { + var engineeringModel = this.CurrentFileStore.GetContainerOfType(); + this.Folder.Creator = engineeringModel.GetActiveParticipant(this.SessionService.Session.ActivePerson); + fileStoreClone.Folder.Add(this.Folder); thingsToCreate.Add(fileStoreClone); } thingsToCreate.Add(this.Folder); - await this.SessionService.UpdateThings(this.CurrentFileStore.Clone(true), thingsToCreate); + await this.SessionService.UpdateThings(fileStoreClone, thingsToCreate); await this.SessionService.RefreshSession(); this.IsLoading = false; } + /// + /// Deletes the current folder + /// + /// A + public async Task DeleteFolder() + { + var clonedContainer = this.Folder.Container.Clone(false); + + await this.SessionService.DeleteThing(clonedContainer, this.Folder); + await this.SessionService.RefreshSession(); + } + /// /// Handles the refresh of the current /// diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs index 1819e435..f7d5dbdf 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs @@ -50,6 +50,11 @@ public interface IFolderHandlerViewModel : IApplicationBaseViewModel /// Folder Folder { get; set; } + /// + /// Gets a collection of the available s + /// + IEnumerable Folders { get; } + /// /// Selects the current /// @@ -70,5 +75,11 @@ public interface IFolderHandlerViewModel : IApplicationBaseViewModel /// the target folders /// A Task MoveFolder(Folder folder, Folder targetFolder); + + /// + /// Deletes the current folder + /// + /// A + Task DeleteFolder(); } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/Rows/FileRevisionRowViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/Rows/FileRevisionRowViewModel.cs new file mode 100644 index 00000000..6a0ed94a --- /dev/null +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/Rows/FileRevisionRowViewModel.cs @@ -0,0 +1,97 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.ViewModels.Components.EngineeringModel.Rows +{ + using CDP4Common.EngineeringModelData; + + using COMETwebapp.ViewModels.Components.Common.Rows; + + using ReactiveUI; + + /// + /// Row View Model for + /// + public class FileRevisionRowViewModel : ReactiveObject + { + /// + /// The backing field for + /// + private string createdOn; + + /// + /// The backing field for + /// + private string path; + + /// + /// The backing field for + /// + private string name; + + /// + /// Initializes a new instance of the class. + /// + /// The associated + public FileRevisionRowViewModel(FileRevision fileRevision) + { + this.Thing = fileRevision; + this.CreatedOn = fileRevision.CreatedOn.ToString("dd/MM/yyyy HH:mm:ss"); + this.Path = fileRevision.Path; + this.Name = fileRevision.Name; + } + + /// + /// Gets or sets the current + /// + public FileRevision Thing { get; set; } + + /// + /// The date and time when the was created, as a + /// + public string CreatedOn + { + get => this.createdOn; + set => this.RaiseAndSetIfChanged(ref this.createdOn, value); + } + + /// + /// The path of the + /// + public string Path + { + get => this.path; + set => this.RaiseAndSetIfChanged(ref this.path, value); + } + + /// + /// The name of the + /// + public string Name + { + get => this.name; + set => this.RaiseAndSetIfChanged(ref this.name, value); + } + } +} diff --git a/COMETwebapp/wwwroot/Scripts/utilities.js b/COMETwebapp/wwwroot/Scripts/utilities.js new file mode 100644 index 00000000..c9babf72 --- /dev/null +++ b/COMETwebapp/wwwroot/Scripts/utilities.js @@ -0,0 +1,21 @@ +/** + * Downloads a given file by its name + * @param {string} fileName + * @param {any} contentStreamReference + */ +var DownloadFileFromStream = async (fileName, contentStreamReference) => { + + const arrayBuffer = await contentStreamReference.arrayBuffer(); + const blob = new Blob([arrayBuffer], { type: 'text/plain;charset=utf-8' }); + + const url = URL.createObjectURL(blob); + + const anchorElement = document.createElement('a'); + + anchorElement.href = url; + anchorElement.download = fileName ?? ''; + anchorElement.target = "_blank"; + anchorElement.click(); + anchorElement.remove(); + URL.revokeObjectURL(url); +} From b225c5a655cf792a452d3212a66da94d5e7d3360 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Mon, 15 Apr 2024 12:20:29 +0100 Subject: [PATCH 04/12] support for multiple file types + improved design for revisions table + code refactored TODO - Validation - Unit tests --- .../FolderFileStructureTestFixture.cs | 4 +- .../CommonFileStoresTable.razor | 2 + .../{ => FileStore}/FileForm.razor | 8 +- .../{ => FileStore}/FileForm.razor.cs | 4 +- .../{ => FileStore}/FileRevisionsTable.razor | 20 +- .../FileRevisionsTable.razor.cs | 81 +------ .../FileStore/FileTypesTable.razor | 71 ++++++ .../FileStore/FileTypesTable.razor.cs | 136 +++++++++++ .../{ => FileStore}/FolderFileStructure.razor | 2 +- .../FolderFileStructure.razor.cs | 4 +- .../{ => FileStore}/FolderForm.razor | 0 .../{ => FileStore}/FolderForm.razor.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 8 +- COMETwebapp/Utilities/Constants.cs | 37 +++ .../CommonFileStoreTableViewModel.cs | 2 +- .../ICommonFileStoreTableViewModel.cs | 2 +- .../FileFolderNodeViewModel.cs | 2 +- .../FileHandler/FileHandlerViewModel.cs | 75 ++---- .../FileHandler/IFileHandlerViewModel.cs | 16 +- .../FileRevisionHandlerViewModel.cs | 221 ++++++++++++++++++ .../IFileRevisionHandlerViewModel.cs | 84 +++++++ .../FolderFileStructureViewModel.cs | 6 +- .../FolderHandler/FolderHandlerViewModel.cs | 2 +- .../FolderHandler/IFolderHandlerViewModel.cs | 2 +- .../IFolderFileStructureViewModel.cs | 6 +- .../Rows/FileTypeRowViewModel.cs} | 48 ++-- COMETwebapp/appsettings.json | 1 + .../31119e59-4abe-4125-a335-b3836b1f5df1 | 1 + 28 files changed, 660 insertions(+), 189 deletions(-) rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FileForm.razor (91%) rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FileForm.razor.cs (95%) rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FileRevisionsTable.razor (79%) rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FileRevisionsTable.razor.cs (62%) create mode 100644 COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor create mode 100644 COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor.cs rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FolderFileStructure.razor (97%) rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FolderFileStructure.razor.cs (98%) rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FolderForm.razor (100%) rename COMETwebapp/Components/EngineeringModel/{ => FileStore}/FolderForm.razor.cs (95%) create mode 100644 COMETwebapp/Utilities/Constants.cs rename COMETwebapp/ViewModels/Components/EngineeringModel/{FolderFileStructure => FileStore}/FileFolderNodeViewModel.cs (98%) rename COMETwebapp/ViewModels/Components/EngineeringModel/{FolderFileStructure => FileStore}/FileHandler/FileHandlerViewModel.cs (79%) rename COMETwebapp/ViewModels/Components/EngineeringModel/{FolderFileStructure => FileStore}/FileHandler/IFileHandlerViewModel.cs (92%) create mode 100644 COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs rename COMETwebapp/ViewModels/Components/EngineeringModel/{FolderFileStructure => FileStore}/FolderFileStructureViewModel.cs (96%) rename COMETwebapp/ViewModels/Components/EngineeringModel/{FolderFileStructure => FileStore}/FolderHandler/FolderHandlerViewModel.cs (98%) rename COMETwebapp/ViewModels/Components/EngineeringModel/{FolderFileStructure => FileStore}/FolderHandler/IFolderHandlerViewModel.cs (97%) rename COMETwebapp/ViewModels/Components/EngineeringModel/{FolderFileStructure => FileStore}/IFolderFileStructureViewModel.cs (90%) rename COMETwebapp/{Extensions/FileExtensions.cs => ViewModels/Components/EngineeringModel/Rows/FileTypeRowViewModel.cs} (53%) create mode 100644 COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs index 1bc8645f..13ddd456 100644 --- a/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs +++ b/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs @@ -33,8 +33,8 @@ namespace COMETwebapp.Tests.Components.EngineeringModel using COMET.Web.Common.Test.Helpers; - using COMETwebapp.Components.EngineeringModel; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; using DevExpress.Blazor; diff --git a/COMETwebapp/Components/EngineeringModel/CommonFileStoresTable.razor b/COMETwebapp/Components/EngineeringModel/CommonFileStoresTable.razor index e4d773bc..5bfd0f5a 100644 --- a/COMETwebapp/Components/EngineeringModel/CommonFileStoresTable.razor +++ b/COMETwebapp/Components/EngineeringModel/CommonFileStoresTable.razor @@ -14,6 +14,8 @@ Copyright (c) 2023-2024 RHEA System S.A. You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/. -------------------------------------------------------------------------------> + +@using COMETwebapp.Components.EngineeringModel.FileStore @inherits SelectedDataItemBase diff --git a/COMETwebapp/Components/EngineeringModel/FileForm.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor similarity index 91% rename from COMETwebapp/Components/EngineeringModel/FileForm.razor rename to COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor index 20861ef6..80146857 100644 --- a/COMETwebapp/Components/EngineeringModel/FileForm.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor @@ -17,7 +17,7 @@ Copyright (c) 2023-2024 RHEA System S.A. @using COMETwebapp.Extensions @inherits SelectedDataItemForm - + @@ -49,10 +49,8 @@ Copyright (c) 2023-2024 RHEA System S.A. - + diff --git a/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor.cs similarity index 95% rename from COMETwebapp/Components/EngineeringModel/FileForm.razor.cs rename to COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor.cs index ff1114b9..706a4ced 100644 --- a/COMETwebapp/Components/EngineeringModel/FileForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor.cs @@ -22,10 +22,10 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.Components.EngineeringModel +namespace COMETwebapp.Components.EngineeringModel.FileStore { using COMETwebapp.Components.Common; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; using Microsoft.AspNetCore.Components; diff --git a/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor similarity index 79% rename from COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor rename to COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor index 6a947a13..40d72890 100644 --- a/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor @@ -18,7 +18,7 @@ Copyright (c) 2023-2024 RHEA System S.A. @inherits DisposableComponent + IconCssClass="oi oi-data-transfer-download" + Click="() => this.ViewModel.DownloadFileRevision(row.Thing)" /> } @@ -50,14 +50,20 @@ Copyright (c) 2023-2024 RHEA System S.A. - $".{x.Extension}")))"> + $".{x.Extension}")))"> - - + + + +
diff --git a/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor.cs b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor.cs similarity index 62% rename from COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor.cs rename to COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor.cs index 8a1545f5..579767c0 100644 --- a/COMETwebapp/Components/EngineeringModel/FileRevisionsTable.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor.cs @@ -22,15 +22,13 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.Components.EngineeringModel +namespace COMETwebapp.Components.EngineeringModel.FileStore { using CDP4Common.EngineeringModelData; - using CDP4Common.SiteDirectoryData; using COMET.Web.Common.Components; - using COMET.Web.Common.Services.SessionManagement; - using COMETwebapp.Extensions; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; using COMETwebapp.ViewModels.Components.EngineeringModel.Rows; using DevExpress.Blazor; @@ -44,10 +42,10 @@ namespace COMETwebapp.Components.EngineeringModel public partial class FileRevisionsTable : DisposableComponent { /// - /// The file to be handled + /// Gets or sets the /// [Parameter] - public File File { get; set; } + public IFileRevisionHandlerViewModel ViewModel { get; set; } /// /// A collection of file revisions to display for selection @@ -55,18 +53,6 @@ public partial class FileRevisionsTable : DisposableComponent [Parameter] public IEnumerable FileRevisions { get; set; } - /// - /// A collection of the acceptable file extensions - /// - [Parameter] - public IEnumerable AcceptableFileExtensions { get; set; } - - /// - /// Method used to download a file revision file - /// - [Parameter] - public Action DownloadFileRevision { get; set; } - /// /// The method that is executed when the file revisions change /// @@ -78,38 +64,13 @@ public partial class FileRevisionsTable : DisposableComponent /// private IGrid Grid { get; set; } - /// - /// The file revision that will be handled for both edit and add forms - /// - private FileRevision FileRevision { get; set; } = new(); - - /// - /// The directory where uploaded files are stored - /// - private const string UploadsDirectory = "wwwroot/uploads/"; - - /// - /// The maximum file size to upload in megabytes - /// - private const double MaxUploadFileSizeInMb = 512; - - /// - /// Gets or sets the uploaded file path - /// - private string UploadedFilePath { get; set; } - - /// - /// Gets or sets the error message that is displayed in the component - /// - public string ErrorMessage { get; private set; } - /// /// Method that is invoked when the edit/add file revision form is being saved /// private async Task OnEditFileRevisionSaving() { var listOfFileRevisions = this.FileRevisions.ToList(); - listOfFileRevisions.Add(this.FileRevision); + listOfFileRevisions.Add(this.ViewModel.FileRevision); this.FileRevisions = listOfFileRevisions; await this.FileRevisionsChanged.InvokeAsync(this.FileRevisions); @@ -120,7 +81,7 @@ private async Task OnEditFileRevisionSaving() ///
private void RemoveFileRevision(FileRevisionRowViewModel row) { - this.File.FileRevision.Remove(row.Thing); + this.ViewModel.CurrentFile.FileRevision.Remove(row.Thing); } /// @@ -130,9 +91,9 @@ private void RemoveFileRevision(FileRevisionRowViewModel row) private void CustomizeEditFileRevision(GridCustomizeEditModelEventArgs e) { var dataItem = (FileRevisionRowViewModel)e.DataItem; - this.FileRevision = dataItem == null ? new FileRevision() : dataItem.Thing; - this.FileRevision.ContainingFolder = this.File.CurrentContainingFolder; - e.EditModel = this.FileRevision; + this.ViewModel.FileRevision = dataItem == null ? new FileRevision() : dataItem.Thing; + this.ViewModel.FileRevision.ContainingFolder = this.ViewModel.CurrentFile.CurrentContainingFolder; + e.EditModel = this.ViewModel.FileRevision; } /// @@ -149,26 +110,7 @@ private List GetRows() /// private async Task OnFileUpload(InputFileChangeEventArgs e) { - var maxUploadFileSizeInBytes = (long)(MaxUploadFileSizeInMb * 1024 * 1024); - - if (e.File.Size > maxUploadFileSizeInBytes) - { - this.ErrorMessage = $"The max file size is {MaxUploadFileSizeInMb} MB"; - return; - } - - this.UploadedFilePath = Path.Combine(UploadsDirectory, Guid.NewGuid().ToString()); - Directory.CreateDirectory(UploadsDirectory); - - await using (var fileStream = new FileStream(this.UploadedFilePath, FileMode.Create)) - { - await e.File.OpenReadStream(maxUploadFileSizeInBytes).CopyToAsync(fileStream); - } - - this.FileRevision.Name = e.File.Name; - this.FileRevision.LocalPath = this.UploadedFilePath; - - await this.InvokeAsync(this.StateHasChanged); + await this.ViewModel.UploadFile(e.File); } /// @@ -177,7 +119,8 @@ private async Task OnFileUpload(InputFileChangeEventArgs e) /// Value asserting if this component should dispose or not protected override void Dispose(bool disposing) { - FileExtensions.TryDelete(this.UploadedFilePath); + base.Dispose(disposing); + this.ViewModel.Dispose(); } } } diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor new file mode 100644 index 00000000..c1d6ca63 --- /dev/null +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor @@ -0,0 +1,71 @@ + +@using COMETwebapp.ViewModels.Components.ReferenceData.Rows +@inherits DisposableComponent + + + + + + + + + + + + @{ + var row = (FileTypeRowViewModel)context.DataItem; + + + + + } + + + + + + + + + + + +
+ +
+
diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor.cs b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor.cs new file mode 100644 index 00000000..213de604 --- /dev/null +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor.cs @@ -0,0 +1,136 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Components.EngineeringModel.FileStore +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + using CDP4Common.Types; + + using COMET.Web.Common.Components; + + using COMETwebapp.ViewModels.Components.EngineeringModel.Rows; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components; + + /// + /// Support class for the + /// + public partial class FileTypesTable : DisposableComponent + { + /// + /// A collection of file types to display for selection + /// + [Parameter] + public IEnumerable FileTypes { get; set; } + + /// + /// A collection of selected file types to display for selection + /// + [Parameter] + public OrderedItemList SelectedFileTypes { get; set; } + + /// + /// The method that is executed when the selected file types change + /// + [Parameter] + public EventCallback> SelectedFileTypesChanged { get; set; } + + /// + /// The file type that will be added + /// + public FileType FileType { get; private set; } + + /// + /// Gets or sets the grid control that is being customized. + /// + private IGrid Grid { get; set; } + + /// + /// Method that is invoked when the edit/add file type form is being saved + /// + private async Task OnEditFileTypesSaving() + { + var listOfFileTypes = this.SelectedFileTypes; + listOfFileTypes.Add(this.FileType); + + this.SelectedFileTypes = listOfFileTypes; + await this.SelectedFileTypesChanged.InvokeAsync(this.SelectedFileTypes); + } + + /// + /// Moves the selected row up + /// + /// The row to be moved + /// A + private async Task MoveUp(FileTypeRowViewModel row) + { + var currentIndex = this.SelectedFileTypes.IndexOf(row.Thing); + this.SelectedFileTypes.Move(currentIndex, currentIndex - 1); + await this.SelectedFileTypesChanged.InvokeAsync(this.SelectedFileTypes); + } + + /// + /// Moves the selected row down + /// + /// The row to be moved + /// A + private async Task MoveDown(FileTypeRowViewModel row) + { + var currentIndex = this.SelectedFileTypes.IndexOf(row.Thing); + this.SelectedFileTypes.Move(currentIndex, currentIndex + 1); + await this.SelectedFileTypesChanged.InvokeAsync(this.SelectedFileTypes); + } + + /// + /// Method that is invoked when a file type row is being removed + /// + private async Task RemoveFileType(FileTypeRowViewModel row) + { + this.SelectedFileTypes.Remove(row.Thing); + await this.SelectedFileTypesChanged.InvokeAsync(this.SelectedFileTypes); + } + + /// + /// Method invoked when creating a new file type + /// + /// A + private void CustomizeEditFileType(GridCustomizeEditModelEventArgs e) + { + this.FileType = new FileType(); + e.EditModel = this.FileType; + } + + /// + /// Method used to retrieve the available rows, given the + /// + /// A collection of s to display + private List GetRows() + { + return this.SelectedFileTypes.Select(x => new FileTypeRowViewModel(x)).ToList(); + } + } +} diff --git a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FolderFileStructure.razor similarity index 97% rename from COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor rename to COMETwebapp/Components/EngineeringModel/FileStore/FolderFileStructure.razor index d20ffe1e..e2d9cc2a 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderFileStructure.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FolderFileStructure.razor @@ -14,7 +14,7 @@ Copyright (c) 2023-2024 RHEA System S.A. You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/. -------------------------------------------------------------------------------> -@using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure +@using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore @inherits DisposableComponent // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.Components.EngineeringModel +namespace COMETwebapp.Components.EngineeringModel.FileStore { using System.ComponentModel.DataAnnotations; @@ -30,7 +30,7 @@ namespace COMETwebapp.Components.EngineeringModel using COMET.Web.Common.Extensions; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; using DevExpress.Blazor; diff --git a/COMETwebapp/Components/EngineeringModel/FolderForm.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor similarity index 100% rename from COMETwebapp/Components/EngineeringModel/FolderForm.razor rename to COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor diff --git a/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor.cs similarity index 95% rename from COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs rename to COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor.cs index 1822be8f..f3eecfcc 100644 --- a/COMETwebapp/Components/EngineeringModel/FolderForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor.cs @@ -22,10 +22,10 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.Components.EngineeringModel +namespace COMETwebapp.Components.EngineeringModel.FileStore { using COMETwebapp.Components.Common; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; using Microsoft.AspNetCore.Components; diff --git a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs index c4262818..0b7d4e18 100644 --- a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs +++ b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs @@ -51,9 +51,10 @@ namespace COMETwebapp.Extensions using COMETwebapp.ViewModels.Components.UserManagement; using COMETwebapp.ViewModels.Components.Viewer; using COMETwebapp.ViewModels.Shared.TopMenuEntry; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; /// /// Extension class for the @@ -117,6 +118,7 @@ public static void RegisterViewModels(this IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } } diff --git a/COMETwebapp/Utilities/Constants.cs b/COMETwebapp/Utilities/Constants.cs new file mode 100644 index 00000000..ac27daf9 --- /dev/null +++ b/COMETwebapp/Utilities/Constants.cs @@ -0,0 +1,37 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Utilities +{ + /// + /// Contains constant values that can be shared across the application + /// + public static class Constants + { + /// + /// The name of the configuration key used to retrieve the max upload file size, in megabytes + /// + public const string MaxUploadFileSizeInMbConfigurationKey = "MaxUploadFileSizeInMb"; + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/CommonFileStoreTableViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/CommonFileStoreTableViewModel.cs index ef75713b..7b31e98a 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/CommonFileStoreTableViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/CommonFileStoreTableViewModel.cs @@ -33,7 +33,7 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.CommonFileStore using COMET.Web.Common.Services.SessionManagement; using COMETwebapp.ViewModels.Components.Common.DeletableDataItemTable; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; using COMETwebapp.ViewModels.Components.EngineeringModel.Rows; /// diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/ICommonFileStoreTableViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/ICommonFileStoreTableViewModel.cs index 73887b0b..22a436b4 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/ICommonFileStoreTableViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/CommonFileStore/ICommonFileStoreTableViewModel.cs @@ -28,7 +28,7 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.CommonFileStore using CDP4Common.SiteDirectoryData; using COMETwebapp.ViewModels.Components.Common.DeletableDataItemTable; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; using COMETwebapp.ViewModels.Components.EngineeringModel.Rows; /// diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileFolderNodeViewModel.cs similarity index 98% rename from COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileFolderNodeViewModel.cs index 94c9dcd9..02287d3a 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileFolderNodeViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileFolderNodeViewModel.cs @@ -22,7 +22,7 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure +namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore { using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs similarity index 79% rename from COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/FileHandlerViewModel.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs index f41f022d..0b5347f1 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/FileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs @@ -22,7 +22,7 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler +namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler { using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; @@ -34,8 +34,7 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure using COMET.Web.Common.ViewModels.Components.Applications; using COMETwebapp.Services.Interoperability; - - using Microsoft.AspNetCore.Routing.Constraints; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; /// /// View model used to manage the files in Filestores @@ -57,15 +56,24 @@ public class FileHandlerViewModel : ApplicationBaseViewModel, IFileHandlerViewMo /// /// The /// The + /// The /// The - public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus, ILogger logger, IJsUtilitiesService jsUtilitiesService) - : base(sessionService, messageBus) + /// The + public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus, ILogger logger, IJsUtilitiesService jsUtilitiesService, + IFileRevisionHandlerViewModel fileRevisionHandlerViewModel) : base(sessionService, messageBus) { this.JsUtilitiesService = jsUtilitiesService; this.logger = logger; + this.FileRevisionHandlerViewModel = fileRevisionHandlerViewModel; + this.InitializeSubscriptions([typeof(File), typeof(Folder)]); } + /// + /// Gets the + /// + public IFileRevisionHandlerViewModel FileRevisionHandlerViewModel { get; private set; } + /// /// Gets or sets the /// @@ -130,6 +138,7 @@ public void SelectFile(File file) this.File = file.Clone(true); this.IsLocked = this.File.LockedBy is not null; this.FileRevisions = this.File.FileRevision; + this.FileRevisionHandlerViewModel.SetFile(this.File); } /// @@ -157,11 +166,6 @@ public async Task MoveFile(File file, Folder targetFolder) this.IsLoading = false; } - public void SetupFileWithNewFileRevisions() - { - - } - /// /// Creates or edits a file /// @@ -173,7 +177,6 @@ public async Task CreateOrEditFile(bool shouldCreate) var thingsToCreate = new List(); var fileStoreClone = this.CurrentFileStore.Clone(false); - var engineeringModel = this.CurrentFileStore.GetContainerOfType(); this.File.LockedBy = this.IsLocked switch { @@ -182,18 +185,13 @@ public async Task CreateOrEditFile(bool shouldCreate) _ => this.File.LockedBy }; + var engineeringModel = this.CurrentFileStore.GetContainerOfType(); var newFileRevisions = this.FileRevisions.Where(x => !this.File.FileRevision.Contains(x)); foreach (var fileRevision in newFileRevisions) { - var fileExtension = Path.GetExtension(fileRevision.Name); - var fileType = engineeringModel.RequiredRdls.SelectMany(x => x.FileType).First(x => $".{x.Extension}" == fileExtension); - - fileRevision.FileType.Add(fileType); - fileRevision.Name = Path.GetFileNameWithoutExtension(fileRevision.Name); fileRevision.Creator = engineeringModel.GetActiveParticipant(this.SessionService.Session.ActivePerson); fileRevision.CreatedOn = DateTime.UtcNow; - fileRevision.ContentHash = CalculateContentHash(fileRevision.LocalPath); this.File.FileRevision.Add(fileRevision); thingsToCreate.Add(fileRevision); @@ -209,9 +207,7 @@ public async Task CreateOrEditFile(bool shouldCreate) await this.SessionService.UpdateThings(fileStoreClone, thingsToCreate); await this.SessionService.RefreshSession(); - - // delete all stored files - + this.IsLoading = false; } @@ -227,49 +223,10 @@ public async Task DeleteFile() await this.SessionService.RefreshSession(); } - /// - /// Downloads a file revision - /// - /// the file revision - /// A - public async Task DownloadFileRevision(FileRevision fileRevision) - { - this.logger.LogInformation("Starting File Revision download..."); - var fileRevisionNameWithExtension = $"{fileRevision.Name}.{fileRevision.FileType.First().Extension}"; - - try - { - var bytes = await this.SessionService.Session.ReadFile(fileRevision); - var stream = new MemoryStream(bytes); - await this.JsUtilitiesService.DownloadFileFromStreamAsync(stream, fileRevisionNameWithExtension); - this.logger.LogInformation("Downloading PDF..."); - } - catch (Exception ex) - { - this.logger.LogError(ex,"File Revision could not be downloaded") ; - } - } - /// /// Handles the refresh of the current /// /// A protected override Task OnSessionRefreshed() => Task.CompletedTask; - - /// - /// Calculates the hash of the file's content - /// - /// the path to the file - /// the hash of the content - private static string CalculateContentHash(string filePath) - { - if (filePath == null) - { - return null; - } - - using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); - return StreamToHashComputer.CalculateSha1HashFromStream(fileStream); - } } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs similarity index 92% rename from COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs index 02e2c0e4..4917f0e9 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FileHandler/IFileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs @@ -22,13 +22,15 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler +namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler { using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; using COMET.Web.Common.ViewModels.Components.Applications; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + /// /// View model used to manage the files in Filestores /// @@ -75,6 +77,11 @@ public interface IFileHandlerViewModel : IApplicationBaseViewModel /// IEnumerable FileTypes { get; } + /// + /// Gets the + /// + IFileRevisionHandlerViewModel FileRevisionHandlerViewModel { get; } + /// /// Moves a file to a target folder /// @@ -101,12 +108,5 @@ public interface IFileHandlerViewModel : IApplicationBaseViewModel /// /// A Task DeleteFile(); - - /// - /// Downloads a file revision - /// - /// the file revision - /// A - Task DownloadFileRevision(FileRevision fileRevision); } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs new file mode 100644 index 00000000..5a4e11e9 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs @@ -0,0 +1,221 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + + using COMET.Web.Common.Services.SessionManagement; + using COMET.Web.Common.ViewModels.Components.Applications; + + using COMETwebapp.Services.Interoperability; + using COMETwebapp.Utilities; + + using Microsoft.AspNetCore.Components.Forms; + + using System.Globalization; + + /// + /// View model used to manage the files revisions in Filestores + /// + public class FileRevisionHandlerViewModel : ApplicationBaseViewModel, IFileRevisionHandlerViewModel + { + /// + /// Gets the + /// + private readonly ILogger logger; + + /// + /// Gets or sets the + /// + private readonly IConfiguration configuration; + + /// + /// Gets or sets the directory where uploaded files for the current file are stored + /// + private readonly string uploadsDirectory; + + /// + /// The maximum file size to upload in megabytes + /// + private double MaxUploadFileSizeInMb => double.Parse(this.configuration.GetSection(Constants.MaxUploadFileSizeInMbConfigurationKey).Value!, CultureInfo.InvariantCulture); + + /// + /// Initializes a new instance of the class. + /// + /// The + /// The + /// The + /// The + /// The + public FileRevisionHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus, ILogger logger, IJsUtilitiesService jsUtilitiesService, + IConfiguration configuration) : base(sessionService, messageBus) + { + this.JsUtilitiesService = jsUtilitiesService; + this.logger = logger; + this.configuration = configuration; + + this.InitializeSubscriptions([typeof(File), typeof(Folder)]); + this.uploadsDirectory = $"wwwroot/uploads/{Guid.NewGuid()}"; + } + + /// + /// Gets or sets the current + /// + public File CurrentFile { get; private set; } + + /// + /// Gets or sets the + /// + public IJsUtilitiesService JsUtilitiesService { get; private set; } + + /// + /// Gets a collection of the selected + /// + public IEnumerable SelectedFileTypes { get; private set; } = Enumerable.Empty(); + + /// + /// Gets a collection of the available s + /// + public IEnumerable FileTypes { get; private set; } + + /// + /// The file revision that will be handled for both edit and add forms + /// + public FileRevision FileRevision { get; set; } = new(); + + /// + /// Gets or sets the error message that is displayed in the component + /// + public string ErrorMessage { get; private set; } + + /// + /// Sets the file for the + /// + /// The to be set + public void SetFile(File file) + { + this.CurrentFile = file; + this.FileTypes = this.SessionService.GetSiteDirectory().AvailableReferenceDataLibraries().SelectMany(x => x.FileType); + } + + /// + /// Downloads a file revision + /// + /// the file revision + /// A + public async Task DownloadFileRevision(FileRevision fileRevision) + { + this.logger.LogInformation("Starting File Revision download..."); + var fileRevisionNameWithExtension = $"{fileRevision.Name}.{string.Join(".", fileRevision.FileType.Select(x => x.Extension))}"; + + try + { + var bytes = await this.SessionService.Session.ReadFile(fileRevision); + var stream = new MemoryStream(bytes); + await this.JsUtilitiesService.DownloadFileFromStreamAsync(stream, fileRevisionNameWithExtension); + this.logger.LogInformation("Downloading PDF..."); + } + catch (Exception ex) + { + this.logger.LogError(ex,"File Revision could not be downloaded") ; + } + } + + /// + /// Uploads a file to server and creates a file revision + /// + /// The file to upload + /// A + public async Task UploadFile(IBrowserFile file) + { + var maxUploadFileSizeInBytes = (long)(this.MaxUploadFileSizeInMb * 1024 * 1024); + + if (file.Size > maxUploadFileSizeInBytes) + { + this.ErrorMessage = $"The max file size is {this.MaxUploadFileSizeInMb} MB"; + return; + } + + var uploadedFilePath = Path.Combine(this.uploadsDirectory, Guid.NewGuid().ToString()); + Directory.CreateDirectory(this.uploadsDirectory); + + await using (var fileStream = new FileStream(uploadedFilePath, FileMode.Create)) + { + await file.OpenReadStream(maxUploadFileSizeInBytes).CopyToAsync(fileStream); + } + + this.FileRevision.Name = Path.GetFileNameWithoutExtension(file.Name); + this.FileRevision.LocalPath = uploadedFilePath; + this.FileRevision.ContentHash = CalculateContentHash(uploadedFilePath); + + var fileExtension = Path.GetExtension(file.Name); + var fileType = this.FileTypes.FirstOrDefault(x => $".{x.Extension}" == fileExtension); + + if (fileType != null) + { + this.FileRevision.FileType.Add(fileType); + } + } + + /// + /// Handles the refresh of the current + /// + /// A + protected override Task OnSessionRefreshed() => Task.CompletedTask; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Value asserting if this component should dispose or not + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (Directory.Exists(this.uploadsDirectory)) + { + Directory.Delete(this.uploadsDirectory, true); + } + } + + /// + /// Calculates the hash of the file's content + /// + /// the path to the file + /// the hash of the content + private static string CalculateContentHash(string filePath) + { + if (filePath == null) + { + return null; + } + + using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + return StreamToHashComputer.CalculateSha1HashFromStream(fileStream); + } + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs new file mode 100644 index 00000000..a85a9b01 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs @@ -0,0 +1,84 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using COMET.Web.Common.ViewModels.Components.Applications; + + using Microsoft.AspNetCore.Components.Forms; + + /// + /// View model used to manage the files revisions in Filestores + /// + public interface IFileRevisionHandlerViewModel : IApplicationBaseViewModel + { + /// + /// Sets the file for the + /// + /// The to be set + void SetFile(File file); + + /// + /// Gets a collection of the selected + /// + IEnumerable SelectedFileTypes { get; } + + /// + /// The file revision that will be handled for both edit and add forms + /// + FileRevision FileRevision { get; set; } + + /// + /// Gets a collection of the available s + /// + IEnumerable FileTypes { get; } + + /// + /// Gets or sets the error message that is displayed in the component + /// + string ErrorMessage { get; } + + /// + /// Gets or sets the current + /// + File CurrentFile { get; } + + /// + /// Downloads a file revision + /// + /// the file revision + /// A + Task DownloadFileRevision(FileRevision fileRevision); + + /// + /// Uploads a file to server and creates a file revision + /// + /// The file to upload + /// A + Task UploadFile(IBrowserFile file); + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderFileStructureViewModel.cs similarity index 96% rename from COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderFileStructureViewModel.cs index ab1819e8..070c3d1a 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderFileStructureViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderFileStructureViewModel.cs @@ -22,7 +22,7 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure +namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore { using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; @@ -32,8 +32,8 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure using COMET.Web.Common.Services.SessionManagement; using COMET.Web.Common.ViewModels.Components.Applications; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; /// /// View model used to manage the folder file structure diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/FolderHandlerViewModel.cs similarity index 98% rename from COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/FolderHandlerViewModel.cs index 562cc4a5..dcb7b9cf 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/FolderHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/FolderHandlerViewModel.cs @@ -22,7 +22,7 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler +namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler { using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/IFolderHandlerViewModel.cs similarity index 97% rename from COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/IFolderHandlerViewModel.cs index f7d5dbdf..c1f8b5a5 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/FolderHandler/IFolderHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/IFolderHandlerViewModel.cs @@ -22,7 +22,7 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler +namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler { using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/IFolderFileStructureViewModel.cs similarity index 90% rename from COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/IFolderFileStructureViewModel.cs index 9a2d30d4..d4436d5a 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FolderFileStructure/IFolderFileStructureViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/IFolderFileStructureViewModel.cs @@ -22,14 +22,14 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure +namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore { using CDP4Common.EngineeringModelData; using COMET.Web.Common.ViewModels.Components.Applications; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FileHandler; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure.FolderHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; /// /// View model used to manage the folder file structure diff --git a/COMETwebapp/Extensions/FileExtensions.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/Rows/FileTypeRowViewModel.cs similarity index 53% rename from COMETwebapp/Extensions/FileExtensions.cs rename to COMETwebapp/ViewModels/Components/EngineeringModel/Rows/FileTypeRowViewModel.cs index 59e55218..78c34768 100644 --- a/COMETwebapp/Extensions/FileExtensions.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/Rows/FileTypeRowViewModel.cs @@ -1,8 +1,8 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) 2023-2024 RHEA System S.A. // -// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua // // This file is part of CDP4-COMET WEB Community Edition // The CDP4-COMET WEB Community Edition is the RHEA Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. @@ -22,29 +22,41 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.Extensions +namespace COMETwebapp.ViewModels.Components.EngineeringModel.Rows { + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using COMETwebapp.ViewModels.Components.Common.Rows; + + using ReactiveUI; + /// - /// Extension class for File + /// Row View Model for /// - public static class FileExtensions + public class FileTypeRowViewModel : BaseDataItemRowViewModel { /// - /// Tries to delete a file in the given file path + /// The backing field for + /// + private string extension; + + /// + /// Initializes a new instance of the class. + /// + /// The associated + public FileTypeRowViewModel(FileType fileType) : base(fileType) + { + this.Extension = fileType.Extension; + } + + /// + /// The file type extension /// - /// The file path - /// true if the file was deleted, otherwise false - public static bool TryDelete(string filePath) + public string Extension { - try - { - File.Delete(filePath); - return true; - } - catch - { - return false; - } + get => this.extension; + set => this.RaiseAndSetIfChanged(ref this.extension, value); } } } diff --git a/COMETwebapp/appsettings.json b/COMETwebapp/appsettings.json index c966947d..28e33cb3 100644 --- a/COMETwebapp/appsettings.json +++ b/COMETwebapp/appsettings.json @@ -1,6 +1,7 @@ { "AllowedHosts": "*", "StringTablePath": "wwwroot/DefaultTextConfiguration.json", + "MaxUploadFileSizeInMb": 500, "ServerConfiguration": { "ServerAddress": "", "BookInputConfiguration": { diff --git a/COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 b/COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 new file mode 100644 index 00000000..8407b89e --- /dev/null +++ b/COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 @@ -0,0 +1 @@ +Extensions > ReSharper > Tools > Template Explorer > File Templates > Razor (C#) \ No newline at end of file From 6204ece02e7736a157274461d9448ef526d1b545 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Mon, 15 Apr 2024 18:07:00 +0100 Subject: [PATCH 05/12] File revision upload fixed TODO - Unit tests --- .../SessionManagement/ISessionService.cs | 9 ++ .../SessionManagement/SessionService.cs | 101 ++++++++++++++++++ .../EngineeringModel/FileStore/FileForm.razor | 4 +- .../FileStore/FileRevisionsTable.razor | 2 +- .../FileStore/FileRevisionsTable.razor.cs | 16 ++- .../FileStore/FileTypesTable.razor | 24 ++++- .../CommonFileStoreValidator.cs | 48 +++++++++ .../EngineeringModel/FileRevisionValidator.cs | 51 +++++++++ .../EngineeringModel/FileTypeValidator.cs | 47 ++++++++ .../EngineeringModel/FileValidator.cs | 48 +++++++++ .../EngineeringModel/FolderValidator.cs | 49 +++++++++ .../FileHandler/FileHandlerViewModel.cs | 51 ++++----- .../FileHandler/IFileHandlerViewModel.cs | 2 +- .../FileRevisionHandlerViewModel.cs | 27 +++-- .../IFileRevisionHandlerViewModel.cs | 3 +- 15 files changed, 434 insertions(+), 48 deletions(-) create mode 100644 COMETwebapp/Validators/EngineeringModel/CommonFileStoreValidator.cs create mode 100644 COMETwebapp/Validators/EngineeringModel/FileRevisionValidator.cs create mode 100644 COMETwebapp/Validators/EngineeringModel/FileTypeValidator.cs create mode 100644 COMETwebapp/Validators/EngineeringModel/FileValidator.cs create mode 100644 COMETwebapp/Validators/EngineeringModel/FolderValidator.cs diff --git a/COMET.Web.Common/Services/SessionManagement/ISessionService.cs b/COMET.Web.Common/Services/SessionManagement/ISessionService.cs index c17ebbcc..ab832b59 100644 --- a/COMET.Web.Common/Services/SessionManagement/ISessionService.cs +++ b/COMET.Web.Common/Services/SessionManagement/ISessionService.cs @@ -239,5 +239,14 @@ public interface ISessionService /// in. /// Task ReadEngineeringModels(IEnumerable engineeringModelSetups); + + /// + /// Creates a new file revision, uploading its physical file to the File Store + /// + /// The that will store the created file + /// The that will contain the file revision + /// The that will be created + /// An asynchronous operation with a + Task CreateFileRevision(FileStore fileStore, File file, FileRevision fileRevision); } } diff --git a/COMET.Web.Common/Services/SessionManagement/SessionService.cs b/COMET.Web.Common/Services/SessionManagement/SessionService.cs index 92da3c6c..5efe14ea 100644 --- a/COMET.Web.Common/Services/SessionManagement/SessionService.cs +++ b/COMET.Web.Common/Services/SessionManagement/SessionService.cs @@ -27,6 +27,7 @@ namespace COMET.Web.Common.Services.SessionManagement { using System.Diagnostics; + using CDP4Common; using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; @@ -318,6 +319,55 @@ public async Task CreateThings(Thing container, IEnumerable thing return result; } + /// + /// Creates a new file revision, uploading its physical file to the File Store + /// + /// The that will store the created file + /// The that will contain the file revision + /// The that will be created + /// An asynchronous operation with a + public async Task CreateFileRevision(FileStore fileStore, File file, FileRevision fileRevision) + { + var result = new Result(); + + try + { + // search for the file in the file store + var transaction = (ThingTransaction)null; + var fileExists = fileStore.File.Any(x => x.Iid == file.Iid); + file.FileRevision.Add(fileRevision); + + if (fileExists) + { + // if the file doesnt exist, include it in the transaction + transaction = this.CreateTransactionForThings(fileStore, [file]); + } + + this.logger.LogInformation("Uploading PDF to File Store"); + + // create transaction for the file revision creation and execute it + transaction = this.CreateTransactionForThings(file, [fileRevision], transaction); + var transactionResult = await this.ExecuteTransaction(transaction, [fileRevision.LocalPath]); + + if (transactionResult.IsFailed) + { + var message = string.Join(". ", transactionResult.Reasons.Select(x => x.Message)); + this.logger.LogError("The PDF can't be uploaded to the File Store. {error}", message); + return transactionResult; + } + + result.WithSuccess(new Success("The PDF was uploaded")); + return result; + } + + catch (Exception ex) + { + this.logger.LogError(ex, "An error ocurred while uploading the PDF to the File Store"); + result.WithError(new Error(ex.Message)); + return result; + } + } + /// /// Write updated Thing in an /// @@ -561,5 +611,56 @@ private List QueryOpenEngineeringModels() return this.OpenIterations.Items.Select(x => (EngineeringModel)x.Container) .DistinctBy(x => x.Iid).ToList(); } + + /// + /// Creates a transaction for the things and the container of those things + /// + /// the container of the things to create + /// the things to create + /// the parent transaction, if null a new transaction is created + /// the + private ThingTransaction CreateTransactionForThings(Thing container, IEnumerable thingsToCreate, ThingTransaction parentTransaction = null) + { + var containerClone = container; + + if (container.Original == null) + { + containerClone = container.Clone(false); + } + + if (parentTransaction == null) + { + var context = TransactionContextResolver.ResolveContext(containerClone); + parentTransaction = new ThingTransaction(context); + } + + thingsToCreate.ToList().ForEach(x => { parentTransaction.Create(x, containerClone); }); + return parentTransaction; + } + + /// + /// Executes and finalizes a transaction. The result is an OperationContainer that the session uses to write the changes + /// + /// the transaction to execute + /// the collection of files + /// an asynchronous operation + private async Task ExecuteTransaction(ThingTransaction transaction, IEnumerable files = null) + { + var operationContainer = transaction.FinalizeTransaction(); + var result = new Result(); + + try + { + await this.Session.Write(operationContainer, files); + this.logger.LogInformation("Writing done!"); + return result; + } + catch (DalWriteException ex) + { + this.logger.LogError("The transaction operation failed: {error}", ex); + result.WithError(new Error(ex.Message)); + return result; + } + } } } diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor index 80146857..dd07e969 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor @@ -49,7 +49,7 @@ Copyright (c) 2023-2024 RHEA System S.A. - @@ -63,7 +63,7 @@ Copyright (c) 2023-2024 RHEA System S.A. Delete + Enabled="@(this.ViewModel.SelectedFileRevisions.Any())"> Save diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor index 40d72890..59e69ee5 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor @@ -18,7 +18,7 @@ Copyright (c) 2023-2024 RHEA System S.A. @inherits DisposableComponent /// Method that is invoked when a file revision row is being removed /// - private void RemoveFileRevision(FileRevisionRowViewModel row) + private async Task RemoveFileRevision(FileRevisionRowViewModel row) { - this.ViewModel.CurrentFile.FileRevision.Remove(row.Thing); + var listOfFileRevisions = this.FileRevisions.ToList(); + listOfFileRevisions.Remove(row.Thing); + + this.FileRevisions = listOfFileRevisions; + await this.FileRevisionsChanged.InvokeAsync(this.FileRevisions); } /// @@ -90,9 +94,11 @@ private void RemoveFileRevision(FileRevisionRowViewModel row) /// A private void CustomizeEditFileRevision(GridCustomizeEditModelEventArgs e) { - var dataItem = (FileRevisionRowViewModel)e.DataItem; - this.ViewModel.FileRevision = dataItem == null ? new FileRevision() : dataItem.Thing; - this.ViewModel.FileRevision.ContainingFolder = this.ViewModel.CurrentFile.CurrentContainingFolder; + this.ViewModel.FileRevision = new FileRevision + { + ContainingFolder = this.ViewModel.CurrentFile.CurrentContainingFolder + }; + e.EditModel = this.ViewModel.FileRevision; } diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor index c1d6ca63..d74baf22 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor @@ -22,7 +22,8 @@ Copyright (c) 2023-2024 RHEA System S.A. EditMode="GridEditMode.PopupEditForm" PopupEditFormHeaderText="File Type" EditModelSaving="@(() => this.OnEditFileTypesSaving())" - CustomizeEditModel="this.CustomizeEditFileType"> + CustomizeEditModel="this.CustomizeEditFileType" + EditFormButtonsVisible="false"> @@ -56,16 +57,31 @@ Copyright (c) 2023-2024 RHEA System S.A. - + + + + CssClass="cw-480"/>
- + +
+ + Save + + + + Cancel + +
+
diff --git a/COMETwebapp/Validators/EngineeringModel/CommonFileStoreValidator.cs b/COMETwebapp/Validators/EngineeringModel/CommonFileStoreValidator.cs new file mode 100644 index 00000000..b05afabf --- /dev/null +++ b/COMETwebapp/Validators/EngineeringModel/CommonFileStoreValidator.cs @@ -0,0 +1,48 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.Validation; + + using COMET.Web.Common.Extensions; + + using FluentValidation; + + /// + /// A class to validate the + /// + public class CommonFileStoreValidator : AbstractValidator + { + /// + /// Instantiates a new + /// + public CommonFileStoreValidator(IValidationService validationService) : base() + { + this.RuleFor(x => x.Name).Validate(validationService, nameof(CommonFileStore.Name)); + this.RuleFor(x => x.Owner).Validate(validationService, nameof(CommonFileStore.Owner)); + } + } +} diff --git a/COMETwebapp/Validators/EngineeringModel/FileRevisionValidator.cs b/COMETwebapp/Validators/EngineeringModel/FileRevisionValidator.cs new file mode 100644 index 00000000..436a33a5 --- /dev/null +++ b/COMETwebapp/Validators/EngineeringModel/FileRevisionValidator.cs @@ -0,0 +1,51 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.Validation; + + using COMET.Web.Common.Extensions; + + using FluentValidation; + + /// + /// A class to validate the + /// + public class FileRevisionValidator : AbstractValidator + { + /// + /// Instantiates a new + /// + public FileRevisionValidator(IValidationService validationService) : base() + { + this.RuleFor(x => x.Name).Validate(validationService, nameof(FileRevision.Name)); + this.RuleFor(x => x.ContainingFolder).Validate(validationService, nameof(FileRevision.ContainingFolder)); + this.RuleFor(x => x.FileType).NotEmpty().Validate(validationService, nameof(FileRevision.FileType)); + this.RuleFor(x => x.LocalPath).NotEmpty().Validate(validationService, nameof(FileRevision.LocalPath)); + this.RuleFor(x => x.Path).NotEmpty().Validate(validationService, nameof(FileRevision.Path)); + } + } +} diff --git a/COMETwebapp/Validators/EngineeringModel/FileTypeValidator.cs b/COMETwebapp/Validators/EngineeringModel/FileTypeValidator.cs new file mode 100644 index 00000000..0e9ca2f0 --- /dev/null +++ b/COMETwebapp/Validators/EngineeringModel/FileTypeValidator.cs @@ -0,0 +1,47 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Validators.EngineeringModel +{ + using CDP4Common.SiteDirectoryData; + using CDP4Common.Validation; + + using COMET.Web.Common.Extensions; + + using FluentValidation; + + /// + /// A class to validate the + /// + public class FileTypeValidator : AbstractValidator + { + /// + /// Instantiates a new + /// + public FileTypeValidator(IValidationService validationService) : base() + { + this.RuleFor(x => x.Extension).Validate(validationService, nameof(FileType.Extension)); + } + } +} diff --git a/COMETwebapp/Validators/EngineeringModel/FileValidator.cs b/COMETwebapp/Validators/EngineeringModel/FileValidator.cs new file mode 100644 index 00000000..43e3b09d --- /dev/null +++ b/COMETwebapp/Validators/EngineeringModel/FileValidator.cs @@ -0,0 +1,48 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.Validation; + + using COMET.Web.Common.Extensions; + + using FluentValidation; + + /// + /// A class to validate the + /// + public class FileValidator : AbstractValidator + { + /// + /// Instantiates a new + /// + public FileValidator(IValidationService validationService) : base() + { + this.RuleFor(x => x.Owner).Validate(validationService, nameof(File.Owner)); + this.RuleFor(x => x.LockedBy).Validate(validationService, nameof(File.LockedBy)); + } + } +} diff --git a/COMETwebapp/Validators/EngineeringModel/FolderValidator.cs b/COMETwebapp/Validators/EngineeringModel/FolderValidator.cs new file mode 100644 index 00000000..3eb72249 --- /dev/null +++ b/COMETwebapp/Validators/EngineeringModel/FolderValidator.cs @@ -0,0 +1,49 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.Validation; + + using COMET.Web.Common.Extensions; + + using FluentValidation; + + /// + /// A class to validate the + /// + public class FolderValidator : AbstractValidator + { + /// + /// Instantiates a new + /// + public FolderValidator(IValidationService validationService) : base() + { + this.RuleFor(x => x.Name).Validate(validationService, nameof(Folder.Name)); + this.RuleFor(x => x.Owner).Validate(validationService, nameof(Folder.Owner)); + this.RuleFor(x => x.ContainingFolder).Validate(validationService, nameof(Folder.ContainingFolder)); + } + } +} diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs index 0b5347f1..f73af5fe 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs @@ -33,7 +33,6 @@ namespace COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandl using COMET.Web.Common.Services.SessionManagement; using COMET.Web.Common.ViewModels.Components.Applications; - using COMETwebapp.Services.Interoperability; using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; /// @@ -57,12 +56,10 @@ public class FileHandlerViewModel : ApplicationBaseViewModel, IFileHandlerViewMo /// The /// The /// The - /// The /// The - public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus, ILogger logger, IJsUtilitiesService jsUtilitiesService, - IFileRevisionHandlerViewModel fileRevisionHandlerViewModel) : base(sessionService, messageBus) + public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus, ILogger logger, IFileRevisionHandlerViewModel fileRevisionHandlerViewModel) + : base(sessionService, messageBus) { - this.JsUtilitiesService = jsUtilitiesService; this.logger = logger; this.FileRevisionHandlerViewModel = fileRevisionHandlerViewModel; @@ -74,11 +71,6 @@ public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messa /// public IFileRevisionHandlerViewModel FileRevisionHandlerViewModel { get; private set; } - /// - /// Gets or sets the - /// - public IJsUtilitiesService JsUtilitiesService { get; private set; } - /// /// Gets a collection of the available /// @@ -102,7 +94,7 @@ public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messa /// /// Gets or sets a collection of the file revisions to be created/edited /// - public IEnumerable FileRevisions { get; set; } + public IEnumerable SelectedFileRevisions { get; set; } /// /// Gets or sets the selected folder to create a file revision @@ -135,10 +127,10 @@ public void InitializeViewModel(FileStore fileStore) /// The file to be set public void SelectFile(File file) { - this.File = file.Clone(true); + this.File = file.Iid != Guid.Empty ? file.Clone(true) : file; this.IsLocked = this.File.LockedBy is not null; - this.FileRevisions = this.File.FileRevision; - this.FileRevisionHandlerViewModel.SetFile(this.File); + this.SelectedFileRevisions = this.File.FileRevision; + this.FileRevisionHandlerViewModel.InitializeViewModel(this.File, this.CurrentFileStore); } /// @@ -175,8 +167,8 @@ public async Task CreateOrEditFile(bool shouldCreate) { this.IsLoading = true; - var thingsToCreate = new List(); - var fileStoreClone = this.CurrentFileStore.Clone(false); + var thingsToUpdate = new List(); + var fileStoreClone = this.CurrentFileStore.Clone(true); this.File.LockedBy = this.IsLocked switch { @@ -185,29 +177,32 @@ public async Task CreateOrEditFile(bool shouldCreate) _ => this.File.LockedBy }; - var engineeringModel = this.CurrentFileStore.GetContainerOfType(); - var newFileRevisions = this.FileRevisions.Where(x => !this.File.FileRevision.Contains(x)); + var fileRevisionsToRemove = this.File.FileRevision.Where(x => !this.SelectedFileRevisions.Contains(x)).ToList(); - foreach (var fileRevision in newFileRevisions) + foreach (var fileRevisionToRemove in fileRevisionsToRemove) { - fileRevision.Creator = engineeringModel.GetActiveParticipant(this.SessionService.Session.ActivePerson); - fileRevision.CreatedOn = DateTime.UtcNow; - - this.File.FileRevision.Add(fileRevision); - thingsToCreate.Add(fileRevision); + this.File.FileRevision.Remove(fileRevisionToRemove); + thingsToUpdate.Add(fileRevisionToRemove); } if (shouldCreate) { fileStoreClone.File.Add(this.File); - thingsToCreate.Add(fileStoreClone); + thingsToUpdate.Add(fileStoreClone); } - thingsToCreate.Add(this.File); + var newFileRevisions = this.SelectedFileRevisions.Where(x => !this.File.FileRevision.Contains(x)).ToList(); - await this.SessionService.UpdateThings(fileStoreClone, thingsToCreate); - await this.SessionService.RefreshSession(); + foreach (var fileRevision in newFileRevisions) + { + await this.SessionService.CreateFileRevision(fileStoreClone, this.File, fileRevision); + } + thingsToUpdate.Add(this.File); + + await this.SessionService.UpdateThings(fileStoreClone, thingsToUpdate); + await this.SessionService.RefreshSession(); + this.IsLoading = false; } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs index 4917f0e9..be821cf8 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/IFileHandlerViewModel.cs @@ -70,7 +70,7 @@ public interface IFileHandlerViewModel : IApplicationBaseViewModel /// /// Gets or sets a collection of the file revisions to be created/edited /// - IEnumerable FileRevisions { get; set; } + IEnumerable SelectedFileRevisions { get; set; } /// /// Gets a collection of the available diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs index 5a4e11e9..72768a75 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs @@ -59,6 +59,11 @@ public class FileRevisionHandlerViewModel : ApplicationBaseViewModel, IFileRevis /// private readonly string uploadsDirectory; + /// + /// Gets or sets the current file store + /// + private FileStore CurrentFileStore { get; set; } + /// /// The maximum file size to upload in megabytes /// @@ -81,6 +86,11 @@ public FileRevisionHandlerViewModel(ISessionService sessionService, ICDPMessageB this.InitializeSubscriptions([typeof(File), typeof(Folder)]); this.uploadsDirectory = $"wwwroot/uploads/{Guid.NewGuid()}"; + + if (!Directory.Exists(this.uploadsDirectory)) + { + Directory.CreateDirectory(this.uploadsDirectory); + } } /// @@ -114,12 +124,14 @@ public FileRevisionHandlerViewModel(ISessionService sessionService, ICDPMessageB public string ErrorMessage { get; private set; } /// - /// Sets the file for the + /// Initializes the current /// /// The to be set - public void SetFile(File file) + /// + public void InitializeViewModel(File file, FileStore fileStore) { this.CurrentFile = file; + this.CurrentFileStore = fileStore; this.FileTypes = this.SessionService.GetSiteDirectory().AvailableReferenceDataLibraries().SelectMany(x => x.FileType); } @@ -162,17 +174,20 @@ public async Task UploadFile(IBrowserFile file) } var uploadedFilePath = Path.Combine(this.uploadsDirectory, Guid.NewGuid().ToString()); - Directory.CreateDirectory(this.uploadsDirectory); await using (var fileStream = new FileStream(uploadedFilePath, FileMode.Create)) { await file.OpenReadStream(maxUploadFileSizeInBytes).CopyToAsync(fileStream); } + var engineeringModel = this.CurrentFileStore.GetContainerOfType(); + this.FileRevision.Name = Path.GetFileNameWithoutExtension(file.Name); this.FileRevision.LocalPath = uploadedFilePath; - this.FileRevision.ContentHash = CalculateContentHash(uploadedFilePath); - + this.FileRevision.ContentHash = CalculateContentHash(this.FileRevision.LocalPath); + this.FileRevision.Creator = engineeringModel.GetActiveParticipant(this.SessionService.Session.ActivePerson); + this.FileRevision.CreatedOn = DateTime.UtcNow; + var fileExtension = Path.GetExtension(file.Name); var fileType = this.FileTypes.FirstOrDefault(x => $".{x.Extension}" == fileExtension); @@ -198,7 +213,7 @@ protected override void Dispose(bool disposing) if (Directory.Exists(this.uploadsDirectory)) { - Directory.Delete(this.uploadsDirectory, true); + // Directory.Delete(this.uploadsDirectory, true); } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs index a85a9b01..866a9632 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/IFileRevisionHandlerViewModel.cs @@ -40,7 +40,8 @@ public interface IFileRevisionHandlerViewModel : IApplicationBaseViewModel /// Sets the file for the /// /// The to be set - void SetFile(File file); + /// + void InitializeViewModel(File file, FileStore fileStore); /// /// Gets a collection of the selected From b662ea2fea269fa9571b94667b8d0ef238b0a62c Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Tue, 16 Apr 2024 10:40:01 +0100 Subject: [PATCH 06/12] override update method in session service + fixed issue with upload of non existing files --- .../SessionManagement/ISessionService.cs | 10 +- .../SessionManagement/SessionService.cs | 124 ++++++------------ .../FileHandler/FileHandlerViewModel.cs | 17 +-- .../FileRevisionHandlerViewModel.cs | 23 ++-- .../31119e59-4abe-4125-a335-b3836b1f5df1 | 1 - 5 files changed, 66 insertions(+), 109 deletions(-) delete mode 100644 COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 diff --git a/COMET.Web.Common/Services/SessionManagement/ISessionService.cs b/COMET.Web.Common/Services/SessionManagement/ISessionService.cs index ab832b59..0a85e236 100644 --- a/COMET.Web.Common/Services/SessionManagement/ISessionService.cs +++ b/COMET.Web.Common/Services/SessionManagement/ISessionService.cs @@ -241,12 +241,12 @@ public interface ISessionService Task ReadEngineeringModels(IEnumerable engineeringModelSetups); /// - /// Creates a new file revision, uploading its physical file to the File Store + /// Write updated Things in an and uploads the given files to the filestore /// - /// The that will store the created file - /// The that will contain the file revision - /// The that will be created + /// The where the s should be updated + /// List of Things to update in the session + /// >A collection of file paths for files to be send to the file store /// An asynchronous operation with a - Task CreateFileRevision(FileStore fileStore, File file, FileRevision fileRevision); + Task UpdateThings(Thing container, IEnumerable thingsToUpdate, IEnumerable files); } } diff --git a/COMET.Web.Common/Services/SessionManagement/SessionService.cs b/COMET.Web.Common/Services/SessionManagement/SessionService.cs index 5efe14ea..724cf05c 100644 --- a/COMET.Web.Common/Services/SessionManagement/SessionService.cs +++ b/COMET.Web.Common/Services/SessionManagement/SessionService.cs @@ -27,7 +27,6 @@ namespace COMET.Web.Common.Services.SessionManagement { using System.Diagnostics; - using CDP4Common; using CDP4Common.CommonData; using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; @@ -320,52 +319,60 @@ public async Task CreateThings(Thing container, IEnumerable thing } /// - /// Creates a new file revision, uploading its physical file to the File Store + /// Write updated Things in an and uploads the given files to the filestore /// - /// The that will store the created file - /// The that will contain the file revision - /// The that will be created + /// The where the s should be updated + /// List of Things to update in the session + /// >A collection of file paths for files to be send to the file store /// An asynchronous operation with a - public async Task CreateFileRevision(FileStore fileStore, File file, FileRevision fileRevision) + public async Task UpdateThings(Thing container, IEnumerable thingsToUpdate, IEnumerable files) { var result = new Result(); - try + if (thingsToUpdate == null) { - // search for the file in the file store - var transaction = (ThingTransaction)null; - var fileExists = fileStore.File.Any(x => x.Iid == file.Iid); - file.FileRevision.Add(fileRevision); + result.Errors.Add(new Error("The things to update can't be null")); + return result; + } - if (fileExists) - { - // if the file doesnt exist, include it in the transaction - transaction = this.CreateTransactionForThings(fileStore, [file]); - } + var sw = Stopwatch.StartNew(); - this.logger.LogInformation("Uploading PDF to File Store"); + // CreateThings a shallow clone of the thing. The cached Thing object should not be changed, so we record the change on a clone. + var thingClone = container; - // create transaction for the file revision creation and execute it - transaction = this.CreateTransactionForThings(file, [fileRevision], transaction); - var transactionResult = await this.ExecuteTransaction(transaction, [fileRevision.LocalPath]); + if (container.Original == null) + { + thingClone = container.Clone(false); + } - if (transactionResult.IsFailed) - { - var message = string.Join(". ", transactionResult.Reasons.Select(x => x.Message)); - this.logger.LogError("The PDF can't be uploaded to the File Store. {error}", message); - return transactionResult; - } + // set the context of the transaction to the thing changes need to be added to. + var context = TransactionContextResolver.ResolveContext(thingClone); + var transaction = new ThingTransaction(context); - result.WithSuccess(new Success("The PDF was uploaded")); - return result; + // register all updates with the transaction. + thingsToUpdate.ToList().ForEach(transaction.CreateOrUpdate); + + // finalize the transaction, the result is an OperationContainer that the session class uses to write the changes to the Thing object. + var operationContainer = transaction.FinalizeTransaction(); + result = new Result(); + + try + { + await this.Session.Write(operationContainer, files); + this.logger.LogInformation("Update writing done in {swElapsedMilliseconds} [ms]", sw.ElapsedMilliseconds); + result.Successes.Add(new Success($"Update writing done in {sw.ElapsedMilliseconds} [ms]")); } - - catch (Exception ex) + catch (DalWriteException ex) { - this.logger.LogError(ex, "An error ocurred while uploading the PDF to the File Store"); - result.WithError(new Error(ex.Message)); - return result; + this.logger.LogError("The update operation failed: {exMessage}", ex.Message); + result.Errors.Add(new Error($"The update operation failed: {ex.Message}")); } + finally + { + sw.Stop(); + } + + return result; } /// @@ -611,56 +618,5 @@ private List QueryOpenEngineeringModels() return this.OpenIterations.Items.Select(x => (EngineeringModel)x.Container) .DistinctBy(x => x.Iid).ToList(); } - - /// - /// Creates a transaction for the things and the container of those things - /// - /// the container of the things to create - /// the things to create - /// the parent transaction, if null a new transaction is created - /// the - private ThingTransaction CreateTransactionForThings(Thing container, IEnumerable thingsToCreate, ThingTransaction parentTransaction = null) - { - var containerClone = container; - - if (container.Original == null) - { - containerClone = container.Clone(false); - } - - if (parentTransaction == null) - { - var context = TransactionContextResolver.ResolveContext(containerClone); - parentTransaction = new ThingTransaction(context); - } - - thingsToCreate.ToList().ForEach(x => { parentTransaction.Create(x, containerClone); }); - return parentTransaction; - } - - /// - /// Executes and finalizes a transaction. The result is an OperationContainer that the session uses to write the changes - /// - /// the transaction to execute - /// the collection of files - /// an asynchronous operation - private async Task ExecuteTransaction(ThingTransaction transaction, IEnumerable files = null) - { - var operationContainer = transaction.FinalizeTransaction(); - var result = new Result(); - - try - { - await this.Session.Write(operationContainer, files); - this.logger.LogInformation("Writing done!"); - return result; - } - catch (DalWriteException ex) - { - this.logger.LogError("The transaction operation failed: {error}", ex); - result.WithError(new Error(ex.Message)); - return result; - } - } } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs index f73af5fe..55c1e4e7 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs @@ -177,6 +177,14 @@ public async Task CreateOrEditFile(bool shouldCreate) _ => this.File.LockedBy }; + var newFileRevisions = this.SelectedFileRevisions.Where(x => !this.File.FileRevision.Contains(x)).ToList(); + + foreach (var fileRevision in newFileRevisions) + { + this.File.FileRevision.Add(fileRevision); + thingsToUpdate.Add(fileRevision); + } + var fileRevisionsToRemove = this.File.FileRevision.Where(x => !this.SelectedFileRevisions.Contains(x)).ToList(); foreach (var fileRevisionToRemove in fileRevisionsToRemove) @@ -191,16 +199,9 @@ public async Task CreateOrEditFile(bool shouldCreate) thingsToUpdate.Add(fileStoreClone); } - var newFileRevisions = this.SelectedFileRevisions.Where(x => !this.File.FileRevision.Contains(x)).ToList(); - - foreach (var fileRevision in newFileRevisions) - { - await this.SessionService.CreateFileRevision(fileStoreClone, this.File, fileRevision); - } - thingsToUpdate.Add(this.File); - await this.SessionService.UpdateThings(fileStoreClone, thingsToUpdate); + await this.SessionService.UpdateThings(fileStoreClone, thingsToUpdate, newFileRevisions.Select(x => x.LocalPath)); await this.SessionService.RefreshSession(); this.IsLoading = false; diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs index 72768a75..e61ac194 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs @@ -57,7 +57,7 @@ public class FileRevisionHandlerViewModel : ApplicationBaseViewModel, IFileRevis /// /// Gets or sets the directory where uploaded files for the current file are stored /// - private readonly string uploadsDirectory; + private string UploadsDirectory { get; set; } /// /// Gets or sets the current file store @@ -85,12 +85,6 @@ public FileRevisionHandlerViewModel(ISessionService sessionService, ICDPMessageB this.configuration = configuration; this.InitializeSubscriptions([typeof(File), typeof(Folder)]); - this.uploadsDirectory = $"wwwroot/uploads/{Guid.NewGuid()}"; - - if (!Directory.Exists(this.uploadsDirectory)) - { - Directory.CreateDirectory(this.uploadsDirectory); - } } /// @@ -133,6 +127,13 @@ public void InitializeViewModel(File file, FileStore fileStore) this.CurrentFile = file; this.CurrentFileStore = fileStore; this.FileTypes = this.SessionService.GetSiteDirectory().AvailableReferenceDataLibraries().SelectMany(x => x.FileType); + + this.UploadsDirectory = $"wwwroot/uploads/{Guid.NewGuid()}"; + + if (!Directory.Exists(this.UploadsDirectory)) + { + Directory.CreateDirectory(this.UploadsDirectory); + } } /// @@ -150,7 +151,7 @@ public async Task DownloadFileRevision(FileRevision fileRevision) var bytes = await this.SessionService.Session.ReadFile(fileRevision); var stream = new MemoryStream(bytes); await this.JsUtilitiesService.DownloadFileFromStreamAsync(stream, fileRevisionNameWithExtension); - this.logger.LogInformation("Downloading PDF..."); + this.logger.LogInformation("Downloading File Revision..."); } catch (Exception ex) { @@ -173,7 +174,7 @@ public async Task UploadFile(IBrowserFile file) return; } - var uploadedFilePath = Path.Combine(this.uploadsDirectory, Guid.NewGuid().ToString()); + var uploadedFilePath = Path.Combine(this.UploadsDirectory, Guid.NewGuid().ToString()); await using (var fileStream = new FileStream(uploadedFilePath, FileMode.Create)) { @@ -211,9 +212,9 @@ protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (Directory.Exists(this.uploadsDirectory)) + if (Directory.Exists(this.UploadsDirectory)) { - // Directory.Delete(this.uploadsDirectory, true); + Directory.Delete(this.UploadsDirectory, true); } } diff --git a/COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 b/COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 deleted file mode 100644 index 8407b89e..00000000 --- a/COMETwebapp/wwwroot/uploads/4b74b68b-9425-41d6-a5af-1396ed25576c/31119e59-4abe-4125-a335-b3836b1f5df1 +++ /dev/null @@ -1 +0,0 @@ -Extensions > ReSharper > Tools > Template Explorer > File Templates > Razor (C#) \ No newline at end of file From b2e90d09f83e0bcea3629a1f6fbaf0004c104996 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Tue, 16 Apr 2024 13:55:20 +0100 Subject: [PATCH 07/12] tests for viewmodels + services TODO - Unit tests for components --- .../SessionServiceTestFixture.cs | 19 +- .../FolderFileStructureTestFixture.cs | 158 +++++++-------- .../JsUtilitiesServiceTestFixture.cs | 58 ++++++ ...ommonFileStoreTableViewModelTestFixture.cs | 2 +- .../FileHandlerViewModelTestFixture.cs | 172 ++++++++++++++++ ...FileRevisionHandlerViewModelTestFixture.cs | 190 ++++++++++++++++++ ...FolderFileStructureViewModelTestFixture.cs | 77 ++++++- .../FolderHandlerViewModelTestFixture.cs | 152 ++++++++++++++ .../FileStore/FileRevisionsTable.razor | 4 + .../FileHandler/FileHandlerViewModel.cs | 8 +- .../FileRevisionHandlerViewModel.cs | 2 + .../FolderHandler/FolderHandlerViewModel.cs | 1 - 12 files changed, 750 insertions(+), 93 deletions(-) create mode 100644 COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs create mode 100644 COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs create mode 100644 COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandlerViewModelTestFixture.cs rename COMETwebapp.Tests/ViewModels/Components/EngineeringModel/{ => FileStore}/FolderFileStructureViewModelTestFixture.cs (53%) create mode 100644 COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FolderHandlerViewModelTestFixture.cs diff --git a/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs b/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs index d2e84bce..f164ea3c 100644 --- a/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs +++ b/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs @@ -33,6 +33,8 @@ namespace COMET.Web.Common.Tests.Services.SessionManagement using CDP4Dal; using CDP4Dal.DAL; + using CDP4Dal.Exceptions; + using CDP4Dal.Operations; using COMET.Web.Common.Enumerations; using COMET.Web.Common.Services.SessionManagement; @@ -324,7 +326,7 @@ public void VerifySwitchDomain() } [Test] - public void VerifyUpdateThings() + public async Task VerifyUpdateThings() { var thingsToUpdate = new List(); this.sessionService.IsSessionOpen = true; @@ -335,10 +337,23 @@ public void VerifyUpdateThings() Owner = this.sessionService.GetDomainOfExpertise(this.iteration) }; + this.iteration.Element.Add(element); + var clone = element.Clone(false); clone.Name = "Satellite"; thingsToUpdate.Add(clone); - Assert.DoesNotThrow(() => this.sessionService.UpdateThings(this.iteration, thingsToUpdate)); + Assert.DoesNotThrowAsync(async() => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate)); + + var filesToUpload = new List { this.uri.LocalPath }; + + Assert.Multiple(() => + { + Assert.DoesNotThrowAsync(async () => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate, filesToUpload)); + this.session.Verify(x => x.Write(It.IsAny(), It.IsAny>()), Times.Once); + }); + + this.session.Setup(x => x.Write(It.IsAny(), It.IsAny>())).Throws(new DalWriteException()); + Assert.DoesNotThrowAsync(async () => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate, filesToUpload)); } [Test] diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs index 13ddd456..4a813c62 100644 --- a/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs +++ b/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs @@ -54,86 +54,86 @@ public class FolderFileStructureTestFixture private Mock viewModel; private List structure; - [SetUp] - public void SetUp() - { - this.context = new TestContext(); - this.viewModel = new Mock(); - - var file = new File(); - - file.FileRevision.Add(new FileRevision() - { - Name = "File Revision 1" - }); - - this.structure = - [ - new FileFolderNodeViewModel(file), - new FileFolderNodeViewModel(new Folder(), [new FileFolderNodeViewModel(file)]), - ]; - - this.viewModel.Setup(x => x.File).Returns(new File()); - this.viewModel.Setup(x => x.Folder).Returns(new Folder()); - this.viewModel.Setup(x => x.Structure).Returns(this.structure); - - this.context.ConfigureDevExpressBlazor(); - - this.renderer = this.context.RenderComponent(parameters => - { - parameters.Add(p => p.ViewModel, this.viewModel.Object); - }); - } - - [TearDown] - public void Teardown() - { - this.context.CleanContext(); - this.context.Dispose(); - } - - [Test] - public void VerifyOnInitialized() - { - Assert.Multiple(() => - { - Assert.That(this.renderer.Instance.ViewModel, Is.EqualTo(this.viewModel.Object)); - Assert.That(this.renderer.Markup, Does.Contain(this.structure.First().Name)); - }); - } - - [Test] - public async Task VerifyEditFile() - { + /* [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.viewModel = new Mock(); + + var file = new File(); + + file.FileRevision.Add(new FileRevision() + { + Name = "File Revision 1" + }); + + this.structure = + [ + new FileFolderNodeViewModel(file), + new FileFolderNodeViewModel(new Folder(), [new FileFolderNodeViewModel(file)]), + ]; + + this.viewModel.Setup(x => x.File).Returns(new File()); + this.viewModel.Setup(x => x.Folder).Returns(new Folder()); + this.viewModel.Setup(x => x.Structure).Returns(this.structure); + + this.context.ConfigureDevExpressBlazor(); + + this.renderer = this.context.RenderComponent(parameters => + { + parameters.Add(p => p.ViewModel, this.viewModel.Object); + }); + } + + [TearDown] + public void Teardown() + { + this.context.CleanContext(); + this.context.Dispose(); + } + + [Test] + public void VerifyOnInitialized() + { + Assert.Multiple(() => + { + Assert.That(this.renderer.Instance.ViewModel, Is.EqualTo(this.viewModel.Object)); + Assert.That(this.renderer.Markup, Does.Contain(this.structure.First().Name)); + }); + } + + [Test] + public async Task VerifyEditFile() + { var treeNodeEventMock = new Mock(); - treeNodeEventMock.Setup(x => x.DataItem).Returns(this.structure.First(x => x.Thing is not File)); - this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); - Assert.That(this.renderer.Instance.SelectedFile, Is.Null); - - treeNodeEventMock.Setup(x => x.DataItem).Returns(this.structure.First(x => x.Thing is File)); - this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); - this.renderer.Render(); - - var form = this.renderer.FindComponent(); - - Assert.Multiple(() => - { - Assert.That(form, Is.Not.Null); - Assert.That(form.Instance.ShouldCreate, Is.EqualTo(false)); - Assert.That(this.renderer.Instance.SelectedFile, Is.Not.Null); - Assert.That(this.renderer.Instance.SelectedFolder, Is.Null); - }); - - // Test behavior in case the submmit is valid => next step - var editForm = form.FindComponent(); - await form.InvokeAsync(editForm.Instance.OnValidSubmit.InvokeAsync); - - var cancelButton = editForm.FindComponents().First(x => x.Instance.Id == "cancelFileButton"); - await editForm.InvokeAsync(cancelButton.Instance.Click.InvokeAsync); - Assert.That(this.renderer.Instance.SelectedFile, Is.Null); - } + treeNodeEventMock.Setup(x => x.DataItem).Returns(this.structure.First(x => x.Thing is not File)); + this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); + Assert.That(this.renderer.Instance.SelectedFile, Is.Null); + + treeNodeEventMock.Setup(x => x.DataItem).Returns(this.structure.First(x => x.Thing is File)); + this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); + this.renderer.Render(); + + var form = this.renderer.FindComponent(); + + Assert.Multiple(() => + { + Assert.That(form, Is.Not.Null); + Assert.That(form.Instance.ShouldCreate, Is.EqualTo(false)); + Assert.That(this.renderer.Instance.SelectedFile, Is.Not.Null); + Assert.That(this.renderer.Instance.SelectedFolder, Is.Null); + }); + + // Test behavior in case the submmit is valid => next step + var editForm = form.FindComponent(); + await form.InvokeAsync(editForm.Instance.OnValidSubmit.InvokeAsync); + + var cancelButton = editForm.FindComponents().First(x => x.Instance.Id == "cancelFileButton"); + await editForm.InvokeAsync(cancelButton.Instance.Click.InvokeAsync); + Assert.That(this.renderer.Instance.SelectedFile, Is.Null); + } - [Test] + [Test] public async Task VerifyEditFolder() { var notFolderRow = this.structure.First(x => x.Thing is not Folder); @@ -162,6 +162,6 @@ public async Task VerifyEditFolder() var cancelButton = editForm.FindComponents().First(x => x.Instance.Id == "cancelFolderButton"); await editForm.InvokeAsync(cancelButton.Instance.Click.InvokeAsync); Assert.That(this.renderer.Instance.SelectedFolder, Is.Null); - } + }*/ } } diff --git a/COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs b/COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs new file mode 100644 index 00000000..f49fefef --- /dev/null +++ b/COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs @@ -0,0 +1,58 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Services.Interoperability +{ + using COMETwebapp.Services.Interoperability; + + using Microsoft.JSInterop; + + using Moq; + + using NUnit.Framework; + + [TestFixture] + public class JsUtilitiesServiceTestFixture + { + private Mock jsRuntimeMock; + private JsUtilitiesService service; + + [SetUp] + public void SetUp() + { + this.jsRuntimeMock = new Mock(); + this.service = new JsUtilitiesService(this.jsRuntimeMock.Object); + } + + [Test] + public async Task VerifyFileDownloadUtility() + { + Assert.ThrowsAsync(async () => await this.service.DownloadFileFromStreamAsync(null, null)); + Assert.ThrowsAsync(async () => await this.service.DownloadFileFromStreamAsync(new MemoryStream(), null)); + + await this.service.DownloadFileFromStreamAsync(new MemoryStream(), "fileTest"); + Assert.That(this.jsRuntimeMock.Invocations.Count, Is.EqualTo(1)); + } + } +} diff --git a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/CommonFileStoreTableViewModelTestFixture.cs b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/CommonFileStoreTableViewModelTestFixture.cs index 157b5353..78d7333e 100644 --- a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/CommonFileStoreTableViewModelTestFixture.cs +++ b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/CommonFileStoreTableViewModelTestFixture.cs @@ -35,7 +35,7 @@ namespace COMETwebapp.Tests.ViewModels.Components.EngineeringModel using COMET.Web.Common.Services.SessionManagement; using COMETwebapp.ViewModels.Components.EngineeringModel.CommonFileStore; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; using Microsoft.Extensions.Logging; diff --git a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs new file mode 100644 index 00000000..923f331b --- /dev/null +++ b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileHandlerViewModelTestFixture.cs @@ -0,0 +1,172 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.ViewModels.Components.EngineeringModel.FileStore +{ + using CDP4Common.CommonData; + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + + using COMET.Web.Common.Services.SessionManagement; + + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + + using Microsoft.Extensions.Logging; + + using Moq; + + using NUnit.Framework; + + [TestFixture] + public class FileHandlerViewModelTestFixture + { + private FileHandlerViewModel viewModel; + private CDPMessageBus messageBus; + private Mock sessionService; + private Mock> logger; + private Mock fileRevisionHandlerViewModel; + private CommonFileStore commonFileStore; + + [SetUp] + public void Setup() + { + this.messageBus = new CDPMessageBus(); + this.sessionService = new Mock(); + this.logger = new Mock>(); + this.fileRevisionHandlerViewModel = new Mock(); + + var person = new Person(); + + var siteDirectory = new SiteDirectory() + { + Domain = + { + new DomainOfExpertise() + { + ShortName = "doe", + Name = "Domain Of Expertise" + } + } + }; + + var engineeringModelSetup = new EngineeringModelSetup() + { + Participant = { new Participant { Person = person } } + }; + + var folder1 = new Folder() + { + Iid = Guid.NewGuid(), + Name = "folder 1" + }; + + var file1 = new File() + { + Iid = Guid.NewGuid(), + CurrentContainingFolder = folder1 + }; + + var file2 = new File() + { + Iid = Guid.NewGuid(), + CurrentContainingFolder = folder1 + }; + + file1.FileRevision.Add(new FileRevision()); + + this.commonFileStore = new CommonFileStore() + { + Name = "CFS", + Folder = { folder1 }, + File = { file1, file2 }, + Owner = siteDirectory.Domain.First(), + Container = new EngineeringModel { EngineeringModelSetup = engineeringModelSetup } + }; + + this.sessionService.Setup(x => x.GetSiteDirectory()).Returns(siteDirectory); + this.sessionService.Setup(x => x.Session.ActivePerson).Returns(person); + this.viewModel = new FileHandlerViewModel(this.sessionService.Object, this.messageBus, this.logger.Object, this.fileRevisionHandlerViewModel.Object); + } + + [TearDown] + public void TearDown() + { + this.viewModel.Dispose(); + this.messageBus.Dispose(); + } + + [Test] + public void VerifyInitializeViewModel() + { + this.viewModel.InitializeViewModel(this.commonFileStore); + + Assert.Multiple(() => + { + Assert.That(this.viewModel.Folders, Has.Count.EqualTo(this.commonFileStore.Folder.Count + 1)); + Assert.That(this.viewModel.Folders, Contains.Item(null)); + Assert.That(this.viewModel.DomainsOfExpertise, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async Task VerifyMoveAndDeleteFile() + { + this.viewModel.InitializeViewModel(this.commonFileStore); + await this.viewModel.MoveFile(this.commonFileStore.File[0], this.commonFileStore.Folder[0]); + this.sessionService.Verify(x => x.UpdateThings(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + this.viewModel.SelectFile(this.commonFileStore.File[0]); + await this.viewModel.DeleteFile(); + this.sessionService.Verify(x => x.DeleteThing(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task VerifyCreateOrEditFolder() + { + this.viewModel.InitializeViewModel(this.commonFileStore); + this.viewModel.SelectFile(this.commonFileStore.File[0]); + + await this.viewModel.CreateOrEditFile(false); + this.sessionService.Verify(x => x.UpdateThings(It.IsAny(), It.Is>(c => !c.OfType().Any()), It.IsAny>()), Times.Once); + + this.viewModel.SelectedFileRevisions = [new FileRevision(){ LocalPath = "/localpath" }]; + this.viewModel.IsLocked = true; + await this.viewModel.CreateOrEditFile(true); + + Assert.Multiple(() => + { + this.sessionService.Verify(x => x.UpdateThings( + It.IsAny(), + It.Is>(c => c.OfType().Any()), + It.Is>(c => c.Contains("/localpath"))) + , Times.Once); + + Assert.That(this.viewModel.File.LockedBy, Is.Not.Null); + }); + } + } +} diff --git a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandlerViewModelTestFixture.cs b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandlerViewModelTestFixture.cs new file mode 100644 index 00000000..25a760f8 --- /dev/null +++ b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandlerViewModelTestFixture.cs @@ -0,0 +1,190 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.ViewModels.Components.EngineeringModel.FileStore +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + + using COMET.Web.Common.Services.SessionManagement; + + using COMETwebapp.Services.Interoperability; + using COMETwebapp.Utilities; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + + using Microsoft.AspNetCore.Components.Forms; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Logging; + + using Moq; + + using NUnit.Framework; + + [TestFixture] + public class FileRevisionHandlerViewModelTestFixture + { + private FileRevisionHandlerViewModel viewModel; + private CDPMessageBus messageBus; + private Mock sessionService; + private Mock> logger; + private Mock jsUtilitiesService; + private CommonFileStore commonFileStore; + private File file; + + [SetUp] + public void Setup() + { + this.messageBus = new CDPMessageBus(); + this.sessionService = new Mock(); + this.logger = new Mock>(); + this.jsUtilitiesService = new Mock(); + + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + [Constants.MaxUploadFileSizeInMbConfigurationKey] = "500" + }) + .Build(); + + var person = new Person(); + + var siteDirectory = new SiteDirectory() + { + Domain = + { + new DomainOfExpertise() + { + ShortName = "doe", + Name = "Domain Of Expertise" + } + }, + }; + + siteDirectory.SiteReferenceDataLibrary.Add(new SiteReferenceDataLibrary() + { + FileType = + { + new FileType() + { + Extension = "txt", + Name = "text/txt", + ShortName = "text/txt" + } + } + }); + + var engineeringModelSetup = new EngineeringModelSetup() + { + Participant = { new Participant { Person = person } } + }; + + var folder1 = new Folder() + { + Iid = Guid.NewGuid(), + Name = "folder 1" + }; + + this.file = new File() + { + Iid = Guid.NewGuid(), + CurrentContainingFolder = folder1 + }; + + var fileRevision = new FileRevision() { Name = "file rev 1", }; + fileRevision.FileType.AddRange(siteDirectory.SiteReferenceDataLibrary.First().FileType); + this.file.FileRevision.Add(fileRevision); + + this.commonFileStore = new CommonFileStore() + { + Name = "CFS", + Folder = { folder1 }, + File = { this.file }, + Owner = siteDirectory.Domain.First(), + Container = new EngineeringModel { EngineeringModelSetup = engineeringModelSetup } + }; + + this.sessionService.Setup(x => x.GetSiteDirectory()).Returns(siteDirectory); + this.sessionService.Setup(x => x.Session.ActivePerson).Returns(person); + this.viewModel = new FileRevisionHandlerViewModel(this.sessionService.Object, this.messageBus, this.logger.Object, this.jsUtilitiesService.Object, configuration); + } + + [TearDown] + public void TearDown() + { + this.viewModel.Dispose(); + this.messageBus.Dispose(); + } + + [Test] + public void VerifyInitializeViewModel() + { + this.viewModel.InitializeViewModel(this.file, this.commonFileStore); + + Assert.Multiple(() => + { + Assert.That(this.viewModel.CurrentFile, Is.EqualTo(this.file)); + Assert.That(this.viewModel.FileRevision, Is.Not.Null); + Assert.That(this.viewModel.ErrorMessage, Is.Empty); + }); + } + + [Test] + public async Task VerifyUploadFile() + { + this.viewModel.InitializeViewModel(this.file, this.commonFileStore); + + Assert.That(this.viewModel.ErrorMessage, Is.Empty); + + var fileMock = new Mock(); + fileMock.Setup(x => x.Size).Returns(1000 * 1024 * 1024); + await this.viewModel.UploadFile(fileMock.Object); + Assert.That(this.viewModel.ErrorMessage, Is.Not.Empty); + + fileMock.Setup(x => x.Size).Returns(1); + fileMock.Setup(x => x.Name).Returns("file.txt"); + fileMock.Setup(x => x.OpenReadStream(It.IsAny(), It.IsAny())).Returns(new MemoryStream()); + await this.viewModel.UploadFile(fileMock.Object); + + Assert.Multiple(() => + { + Assert.That(this.viewModel.ErrorMessage, Is.Empty); + Assert.That(this.viewModel.FileRevision.Name, Is.EqualTo("file")); + Assert.That(this.viewModel.FileRevision.FileType.First().Extension, Is.EqualTo("txt")); + }); + } + + [Test] + public async Task VerifyDownloadFile() + { + this.viewModel.InitializeViewModel(this.file, this.commonFileStore); + await this.viewModel.DownloadFileRevision(this.file.CurrentFileRevision); + this.jsUtilitiesService.Verify(x => x.DownloadFileFromStreamAsync(It.IsAny(), It.IsAny()), Times.Once); + + this.sessionService.Setup(x => x.Session.ReadFile(It.IsAny())).Throws(new Exception()); + await this.viewModel.DownloadFileRevision(this.file.CurrentFileRevision); + this.jsUtilitiesService.Verify(x => x.DownloadFileFromStreamAsync(It.IsAny(), It.IsAny()), Times.Once); + } + } +} diff --git a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FolderFileStructureViewModelTestFixture.cs b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FolderFileStructureViewModelTestFixture.cs similarity index 53% rename from COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FolderFileStructureViewModelTestFixture.cs rename to COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FolderFileStructureViewModelTestFixture.cs index 19fedb3f..fbf1fe66 100644 --- a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FolderFileStructureViewModelTestFixture.cs +++ b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FolderFileStructureViewModelTestFixture.cs @@ -22,14 +22,20 @@ // // -------------------------------------------------------------------------------------------------------------------- -namespace COMETwebapp.Tests.ViewModels.Components.EngineeringModel +namespace COMETwebapp.Tests.ViewModels.Components.EngineeringModel.FileStore { using CDP4Common.EngineeringModelData; using CDP4Common.SiteDirectoryData; + using CDP4Dal; + using CDP4Dal.Events; + + using COMET.Web.Common.Enumerations; using COMET.Web.Common.Services.SessionManagement; - using COMETwebapp.ViewModels.Components.EngineeringModel.FolderFileStructure; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; using Moq; @@ -39,13 +45,19 @@ namespace COMETwebapp.Tests.ViewModels.Components.EngineeringModel public class FolderFileStructureViewModelTestFixture { private FolderFileStructureViewModel viewModel; + private CDPMessageBus messageBus; private Mock sessionService; + private Mock fileHandlerViewModel; + private Mock folderHandlerViewModel; private CommonFileStore commonFileStore; [SetUp] public void Setup() { + this.messageBus = new CDPMessageBus(); this.sessionService = new Mock(); + this.fileHandlerViewModel = new Mock(); + this.folderHandlerViewModel = new Mock(); var siteDirectory = new SiteDirectory() { @@ -61,16 +73,16 @@ public void Setup() var folder1 = new Folder() { + Iid = Guid.NewGuid(), Name = "folder 1" }; - var folder2 = new Folder() + var file = new File() { - Name = "folder 2", - ContainingFolder = folder1 + Iid = Guid.NewGuid(), + CurrentContainingFolder = folder1 }; - var file = new File() { CurrentContainingFolder = folder1 }; file.FileRevision.Add(new FileRevision()); this.commonFileStore = new CommonFileStore() @@ -82,7 +94,14 @@ public void Setup() }; this.sessionService.Setup(x => x.GetSiteDirectory()).Returns(siteDirectory); - this.viewModel = new FolderFileStructureViewModel(this.sessionService.Object); + this.viewModel = new FolderFileStructureViewModel(this.sessionService.Object, this.messageBus, this.fileHandlerViewModel.Object, this.folderHandlerViewModel.Object); + } + + [TearDown] + public void TearDown() + { + this.viewModel.Dispose(); + this.messageBus.Dispose(); } [Test] @@ -99,9 +118,51 @@ public void VerifyInitializeViewModel() */ Assert.Multiple(() => { - Assert.That(this.viewModel.DomainsOfExpertise.Count(), Is.GreaterThan(0)); Assert.That(this.viewModel.Structure, Has.Count.EqualTo(1)); Assert.That(this.viewModel.Structure.First().Content, Has.Count.EqualTo(2)); + this.fileHandlerViewModel.Verify(x => x.InitializeViewModel(this.commonFileStore)); + this.folderHandlerViewModel.Verify(x => x.InitializeViewModel(this.commonFileStore)); + }); + } + + [Test] + public void VerifySessionRefresh() + { + this.viewModel.InitializeViewModel(this.commonFileStore); + var rootNodeContent = this.viewModel.Structure.First().Content; + Assert.That(rootNodeContent, Has.Count.EqualTo(2)); + + this.messageBus.SendMessage(SessionStateKind.RefreshEnded); + Assert.That(rootNodeContent, Has.Count.EqualTo(2)); + + var newFile = new File(); + this.messageBus.SendObjectChangeEvent(newFile, EventKind.Added); + this.messageBus.SendMessage(SessionStateKind.RefreshEnded); + + Assert.Multiple(() => + { + Assert.That(rootNodeContent.Select(x => x.Thing), Contains.Item(newFile)); + Assert.That(rootNodeContent, Has.Count.EqualTo(3)); + }); + + var existingFile = this.commonFileStore.File.First(); + existingFile.LockedBy = new Person() { ShortName = "locker" }; + this.messageBus.SendObjectChangeEvent(newFile, EventKind.Updated); + this.messageBus.SendMessage(SessionStateKind.RefreshEnded); + + Assert.Multiple(() => + { + Assert.That(rootNodeContent.Select(x => x.Thing).OfType().Any(x => x.LockedBy.ShortName == "locker"), Is.EqualTo(true)); + Assert.That(rootNodeContent, Has.Count.EqualTo(3)); + }); + + this.messageBus.SendObjectChangeEvent(newFile, EventKind.Removed); + this.messageBus.SendMessage(SessionStateKind.RefreshEnded); + + Assert.Multiple(() => + { + Assert.That(rootNodeContent.Select(x => x.Thing), Does.Not.Contain(newFile)); + Assert.That(rootNodeContent, Has.Count.EqualTo(2)); }); } } diff --git a/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FolderHandlerViewModelTestFixture.cs b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FolderHandlerViewModelTestFixture.cs new file mode 100644 index 00000000..1d565b99 --- /dev/null +++ b/COMETwebapp.Tests/ViewModels/Components/EngineeringModel/FileStore/FolderHandlerViewModelTestFixture.cs @@ -0,0 +1,152 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.ViewModels.Components.EngineeringModel.FileStore +{ + using CDP4Common.CommonData; + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + + using COMET.Web.Common.Services.SessionManagement; + + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; + + using Moq; + + using NUnit.Framework; + + [TestFixture] + public class FolderHandlerViewModelTestFixture + { + private FolderHandlerViewModel viewModel; + private CDPMessageBus messageBus; + private Mock sessionService; + private CommonFileStore commonFileStore; + + [SetUp] + public void Setup() + { + this.messageBus = new CDPMessageBus(); + this.sessionService = new Mock(); + + var person = new Person(); + + var siteDirectory = new SiteDirectory() + { + Domain = + { + new DomainOfExpertise() + { + ShortName = "doe", + Name = "Domain Of Expertise" + } + } + }; + + var engineeringModelSetup = new EngineeringModelSetup() + { + Participant = { new Participant { Person = person } } + }; + + var folder1 = new Folder() + { + Iid = Guid.NewGuid(), + Name = "folder 1" + }; + + var folder2 = new Folder() + { + Iid = Guid.NewGuid(), + Name = "folder 2" + }; + + var file = new File() + { + Iid = Guid.NewGuid(), + CurrentContainingFolder = folder1 + }; + + file.FileRevision.Add(new FileRevision()); + + this.commonFileStore = new CommonFileStore() + { + Name = "CFS", + Folder = { folder1, folder2 }, + File = { file }, + Owner = siteDirectory.Domain.First(), + Container = new EngineeringModel { EngineeringModelSetup = engineeringModelSetup } + }; + + this.sessionService.Setup(x => x.GetSiteDirectory()).Returns(siteDirectory); + this.sessionService.Setup(x => x.Session.ActivePerson).Returns(person); + this.viewModel = new FolderHandlerViewModel(this.sessionService.Object, this.messageBus); + } + + [TearDown] + public void TearDown() + { + this.viewModel.Dispose(); + this.messageBus.Dispose(); + } + + [Test] + public void VerifyInitializeViewModel() + { + this.viewModel.InitializeViewModel(this.commonFileStore); + + Assert.Multiple(() => + { + Assert.That(this.viewModel.Folders, Has.Count.EqualTo(this.commonFileStore.Folder.Count + 1)); + Assert.That(this.viewModel.Folders, Contains.Item(null)); + Assert.That(this.viewModel.DomainsOfExpertise, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async Task VerifyMoveAndDeleteFolder() + { + this.viewModel.InitializeViewModel(this.commonFileStore); + await this.viewModel.MoveFolder(this.commonFileStore.Folder[0], this.commonFileStore.Folder[1]); + this.sessionService.Verify(x => x.UpdateThings(It.IsAny(), It.IsAny()), Times.Once); + + this.viewModel.SelectFolder(this.commonFileStore.Folder[0]); + await this.viewModel.DeleteFolder(); + this.sessionService.Verify(x => x.DeleteThing(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task VerifyCreateOrEditFolder() + { + this.viewModel.InitializeViewModel(this.commonFileStore); + this.viewModel.SelectFolder(this.commonFileStore.Folder[0]); + await this.viewModel.CreateOrEditFolder(false); + this.sessionService.Verify(x => x.UpdateThings(It.IsAny(), It.Is>(c => !c.OfType().Any())), Times.Once); + + await this.viewModel.CreateOrEditFolder(true); + this.sessionService.Verify(x => x.UpdateThings(It.IsAny(), It.Is>(c => c.OfType().Any())), Times.Once); + } + } +} diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor index 59e69ee5..7cf9428f 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileRevisionsTable.razor @@ -67,6 +67,10 @@ Copyright (c) 2023-2024 RHEA System S.A.
+ @if (!string.IsNullOrWhiteSpace(this.ViewModel.ErrorMessage)) + { + @this.ViewModel.ErrorMessage + } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs index 55c1e4e7..6ee8e7c2 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs @@ -62,8 +62,6 @@ public FileHandlerViewModel(ISessionService sessionService, ICDPMessageBus messa { this.logger = logger; this.FileRevisionHandlerViewModel = fileRevisionHandlerViewModel; - - this.InitializeSubscriptions([typeof(File), typeof(Folder)]); } /// @@ -131,6 +129,7 @@ public void SelectFile(File file) this.IsLocked = this.File.LockedBy is not null; this.SelectedFileRevisions = this.File.FileRevision; this.FileRevisionHandlerViewModel.InitializeViewModel(this.File, this.CurrentFileStore); + this.SelectedFolder = null; } /// @@ -181,6 +180,11 @@ public async Task CreateOrEditFile(bool shouldCreate) foreach (var fileRevision in newFileRevisions) { + if (shouldCreate) + { + fileRevision.ContainingFolder = this.SelectedFolder; + } + this.File.FileRevision.Add(fileRevision); thingsToUpdate.Add(fileRevision); } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs index e61ac194..f35a1354 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs @@ -127,6 +127,7 @@ public void InitializeViewModel(File file, FileStore fileStore) this.CurrentFile = file; this.CurrentFileStore = fileStore; this.FileTypes = this.SessionService.GetSiteDirectory().AvailableReferenceDataLibraries().SelectMany(x => x.FileType); + this.ErrorMessage = string.Empty; this.UploadsDirectory = $"wwwroot/uploads/{Guid.NewGuid()}"; @@ -166,6 +167,7 @@ public async Task DownloadFileRevision(FileRevision fileRevision) /// A public async Task UploadFile(IBrowserFile file) { + this.ErrorMessage = string.Empty; var maxUploadFileSizeInBytes = (long)(this.MaxUploadFileSizeInMb * 1024 * 1024); if (file.Size > maxUploadFileSizeInBytes) diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/FolderHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/FolderHandlerViewModel.cs index dcb7b9cf..4b029404 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/FolderHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FolderHandler/FolderHandlerViewModel.cs @@ -50,7 +50,6 @@ public class FolderHandlerViewModel : ApplicationBaseViewModel, IFolderHandlerVi /// The public FolderHandlerViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) { - this.InitializeSubscriptions([typeof(File), typeof(Folder)]); } /// From 2452b5e8ed8521fc95d56a2376a42143bfde2093 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Tue, 16 Apr 2024 17:21:59 +0100 Subject: [PATCH 08/12] unit tests for components --- .../FileStore/FileFormTestFixture.cs | 100 +++++++++ .../FileRevisionsTableTestFixture.cs | 124 +++++++++++ .../FileStore/FileTypesTableTestFixture.cs | 160 ++++++++++++++ .../FolderFileStructureTestFixture.cs | 208 ++++++++++++++++++ .../FileStore/FolderFormTestFixture.cs | 100 +++++++++ .../FolderFileStructureTestFixture.cs | 167 -------------- .../EngineeringModel/FileStore/FileForm.razor | 2 +- .../FileStore/FileForm.razor.cs | 4 +- .../FileStore/FileTypesTable.razor | 2 +- .../FileStore/FolderForm.razor | 2 +- .../FileStore/FolderForm.razor.cs | 4 +- .../FileRevisionHandlerViewModel.cs | 2 - 12 files changed, 699 insertions(+), 176 deletions(-) create mode 100644 COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs create mode 100644 COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileRevisionsTableTestFixture.cs create mode 100644 COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileTypesTableTestFixture.cs create mode 100644 COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFileStructureTestFixture.cs create mode 100644 COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFormTestFixture.cs delete mode 100644 COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs new file mode 100644 index 00000000..b2bf9271 --- /dev/null +++ b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileFormTestFixture.cs @@ -0,0 +1,100 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Components.EngineeringModel.FileStore +{ + using System.Linq; + + using Bunit; + + using CDP4Common.EngineeringModelData; + + using COMET.Web.Common.Test.Helpers; + + using COMETwebapp.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components.Forms; + + using Moq; + + using NUnit.Framework; + + using TestContext = Bunit.TestContext; + + [TestFixture] + public class FileFormTestFixture + { + private TestContext context; + private IRenderedComponent renderer; + private Mock viewModel; + + [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.viewModel = new Mock(); + + this.viewModel.Setup(x => x.File).Returns(new File()); + this.context.ConfigureDevExpressBlazor(); + + this.renderer = this.context.RenderComponent(parameters => + { + parameters.Add(p => p.ViewModel, this.viewModel.Object); + }); + } + + [TearDown] + public void Teardown() + { + this.context.CleanContext(); + this.context.Dispose(); + } + + [Test] + public async Task VerifyOnValidSubmitAndDelete() + { + Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(false)); + + var form = this.renderer.FindComponent(); + await this.renderer.InvokeAsync(form.Instance.OnValidSubmit.InvokeAsync); + this.viewModel.Verify(x => x.CreateOrEditFile(It.IsAny()), Times.Once); + + var deleteFileButton = this.renderer.FindComponents().First(x => x.Instance.Id == "deleteFileButton"); + await this.renderer.InvokeAsync(deleteFileButton.Instance.Click.InvokeAsync); + Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(true)); + + var deleteFilePopupButton = this.renderer.FindComponents().First(x => x.Instance.Id == "deleteFilePopupButton"); + await this.renderer.InvokeAsync(deleteFilePopupButton.Instance.Click.InvokeAsync); + + Assert.Multiple(() => + { + Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(false)); + this.viewModel.Verify(x => x.DeleteFile(), Times.Once); + }); + } + } +} diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileRevisionsTableTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileRevisionsTableTestFixture.cs new file mode 100644 index 00000000..9be2ef36 --- /dev/null +++ b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileRevisionsTableTestFixture.cs @@ -0,0 +1,124 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Components.EngineeringModel.FileStore +{ + using System.Linq; + + using Bunit; + + using CDP4Common.EngineeringModelData; + + using COMET.Web.Common.Test.Helpers; + + using COMETwebapp.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components.Forms; + + using Moq; + + using NUnit.Framework; + + using TestContext = Bunit.TestContext; + + [TestFixture] + public class FileRevisionsTableTestFixture + { + private TestContext context; + private IRenderedComponent renderer; + private Mock viewModel; + private List fileRevisions; + + [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.viewModel = new Mock(); + this.fileRevisions = [new FileRevision()]; + + this.viewModel.Setup(x => x.FileRevision).Returns(new FileRevision()); + this.viewModel.Setup(x => x.CurrentFile).Returns(new File()); + this.context.ConfigureDevExpressBlazor(); + + this.renderer = this.context.RenderComponent(parameters => + { + parameters.Add(p => p.ViewModel, this.viewModel.Object); + parameters.Add(p => p.FileRevisions, this.fileRevisions); + }); + } + + [TearDown] + public void Teardown() + { + this.context.CleanContext(); + this.context.Dispose(); + } + + [Test] + public async Task VerifyRowActions() + { + var timesFileRevisionsWasChanged = 0; + + this.renderer.SetParametersAndRender(parameters => + { + parameters.Add(p => p.FileRevisionsChanged, () => { timesFileRevisionsWasChanged += 1; }); + }); + + var downloadButton = this.renderer.FindComponents().First(x => x.Instance.Id == "downloadFileRevisionButton"); + await this.renderer.InvokeAsync(downloadButton.Instance.Click.InvokeAsync); + this.viewModel.Verify(x => x.DownloadFileRevision(It.IsAny()), Times.Once); + + var removeFileRevisionButton = this.renderer.FindComponents().First(x => x.Instance.Id == "removeFileRevisionButton"); + await this.renderer.InvokeAsync(removeFileRevisionButton.Instance.Click.InvokeAsync); + Assert.That(timesFileRevisionsWasChanged, Is.EqualTo(1)); + } + + [Test] + public async Task VerifyFileRevisionCreation() + { + var timesFileRevisionsWasChanged = 0; + + this.renderer.SetParametersAndRender(parameters => + { + parameters.Add(p => p.FileRevisionsChanged, () => { timesFileRevisionsWasChanged += 1; }); + }); + + var addFileRevisionButton = this.renderer.FindComponents().First(x => x.Instance.Id == "addFileRevisionButton"); + await this.renderer.InvokeAsync(addFileRevisionButton.Instance.Click.InvokeAsync); + + var fileInput = this.renderer.FindComponent(); + var fileMock = new Mock(); + var changeArgs = new InputFileChangeEventArgs([fileMock.Object]); + await this.renderer.InvokeAsync(() => fileInput.Instance.OnChange.InvokeAsync(changeArgs)); + this.viewModel.Verify(x => x.UploadFile(fileMock.Object), Times.Once); + + var grid = this.renderer.FindComponent(); + await this.renderer.InvokeAsync(grid.Instance.EditModelSaving.InvokeAsync); + Assert.That(timesFileRevisionsWasChanged, Is.EqualTo(1)); + } + } +} diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileTypesTableTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileTypesTableTestFixture.cs new file mode 100644 index 00000000..afb026c8 --- /dev/null +++ b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FileTypesTableTestFixture.cs @@ -0,0 +1,160 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Components.EngineeringModel.FileStore +{ + using System.Linq; + + using Bunit; + + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using COMET.Web.Common.Test.Helpers; + + using COMETwebapp.Components.EngineeringModel.FileStore; + + using DevExpress.Blazor; + + using NUnit.Framework; + + using TestContext = Bunit.TestContext; + + [TestFixture] + public class FileTypesTableTestFixture + { + private TestContext context; + private IRenderedComponent renderer; + private FileRevision fileRevision; + + [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.fileRevision = new FileRevision(); + + var pdfFileType = new FileType() + { + Name = "application/pdf", + ShortName = "application/pdf", + Extension = "pdf" + }; + + var jsonFileType = new FileType() + { + Name = "application/json", + ShortName = "application/json", + Extension = "json" + }; + + var textFileType = new FileType() + { + Name = "application/txt", + ShortName = "application/txt", + Extension = "txt" + }; + + var siteRdl = new SiteReferenceDataLibrary() + { + FileType = { pdfFileType, jsonFileType, textFileType } + }; + + this.fileRevision.FileType.AddRange([pdfFileType, jsonFileType]); + this.context.ConfigureDevExpressBlazor(); + + this.renderer = this.context.RenderComponent(parameters => + { + parameters.Add(p => p.FileTypes, siteRdl.FileType); + parameters.Add(p => p.SelectedFileTypes, this.fileRevision.FileType); + }); + } + + [TearDown] + public void Teardown() + { + this.context.CleanContext(); + this.context.Dispose(); + } + + [Test] + public async Task VerifyRowActions() + { + var timesSelectedFileTypesWasChanged = 0; + + this.renderer.SetParametersAndRender(parameters => + { + parameters.Add(p => p.SelectedFileTypesChanged, () => { timesSelectedFileTypesWasChanged += 1; }); + }); + + Assert.Multiple(() => + { + Assert.That(this.fileRevision.FileType[0].Extension, Is.EqualTo("pdf")); + Assert.That(this.fileRevision.FileType[1].Extension, Is.EqualTo("json")); + Assert.That(timesSelectedFileTypesWasChanged, Is.EqualTo(0)); + }); + + var moveUpButton = this.renderer.FindComponents().First(x => x.Instance.Id == "moveUpButton" && x.Instance.Enabled); + await this.renderer.InvokeAsync(moveUpButton.Instance.Click.InvokeAsync); + + Assert.Multiple(() => + { + Assert.That(this.fileRevision.FileType[0].Extension, Is.EqualTo("json")); + Assert.That(this.fileRevision.FileType[1].Extension, Is.EqualTo("pdf")); + Assert.That(timesSelectedFileTypesWasChanged, Is.EqualTo(1)); + }); + + var moveDownButton = this.renderer.FindComponents().First(x => x.Instance.Id == "moveDownButton" && x.Instance.Enabled); + await this.renderer.InvokeAsync(moveDownButton.Instance.Click.InvokeAsync); + + Assert.Multiple(() => + { + Assert.That(this.fileRevision.FileType[0].Extension, Is.EqualTo("pdf")); + Assert.That(this.fileRevision.FileType[1].Extension, Is.EqualTo("json")); + Assert.That(timesSelectedFileTypesWasChanged, Is.EqualTo(2)); + }); + + var removeFileTypeButton = this.renderer.FindComponents().First(x => x.Instance.Id == "removeFileTypeButton"); + await this.renderer.InvokeAsync(removeFileTypeButton.Instance.Click.InvokeAsync); + Assert.That(timesSelectedFileTypesWasChanged, Is.EqualTo(3)); + } + + [Test] + public async Task VerifyFileTypeCreation() + { + var timesSelectedFileTypesWasChanged = 0; + + this.renderer.SetParametersAndRender(parameters => + { + parameters.Add(p => p.SelectedFileTypesChanged, () => { timesSelectedFileTypesWasChanged += 1; }); + }); + + var addFileTypeButton = this.renderer.FindComponents().First(x => x.Instance.Id == "addFileTypeButton"); + await this.renderer.InvokeAsync(addFileTypeButton.Instance.Click.InvokeAsync); + + var grid = this.renderer.FindComponent(); + await this.renderer.InvokeAsync(grid.Instance.EditModelSaving.InvokeAsync); + Assert.That(timesSelectedFileTypesWasChanged, Is.EqualTo(1)); + } + } +} diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFileStructureTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFileStructureTestFixture.cs new file mode 100644 index 00000000..20901304 --- /dev/null +++ b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFileStructureTestFixture.cs @@ -0,0 +1,208 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Components.EngineeringModel.FileStore +{ + using System.Linq; + + using Bunit; + + using CDP4Common.EngineeringModelData; + + using COMET.Web.Common.Test.Helpers; + + using COMETwebapp.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components.Web; + + using Moq; + + using NUnit.Framework; + + using TestContext = Bunit.TestContext; + + [TestFixture] + public class FolderFileStructureTestFixture + { + private TestContext context; + private IRenderedComponent renderer; + private Mock viewModel; + private Mock fileHandlerViewModel; + private Mock folderHandlerViewModel; + private List structure; + + [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.viewModel = new Mock(); + this.folderHandlerViewModel = new Mock(); + this.fileHandlerViewModel = new Mock(); + + var file = new File(); + + file.FileRevision.Add(new FileRevision() + { + Name = "File Revision 1" + }); + + this.structure = + [ + new FileFolderNodeViewModel() + { + Content = + { + new FileFolderNodeViewModel(file), + new FileFolderNodeViewModel(new Folder(), [new FileFolderNodeViewModel(file)]) + } + } + ]; + + this.fileHandlerViewModel.Setup(x => x.File).Returns(new File()); + this.folderHandlerViewModel.Setup(x => x.Folder).Returns(new Folder()); + + this.viewModel.Setup(x => x.Structure).Returns(this.structure); + this.viewModel.Setup(x => x.FileHandlerViewModel).Returns(this.fileHandlerViewModel.Object); + this.viewModel.Setup(x => x.FolderHandlerViewModel).Returns(this.folderHandlerViewModel.Object); + + this.context.ConfigureDevExpressBlazor(); + + this.renderer = this.context.RenderComponent(parameters => + { + parameters.Add(p => p.ViewModel, this.viewModel.Object); + }); + } + + [TearDown] + public void Teardown() + { + this.context.CleanContext(); + this.context.Dispose(); + } + + [Test] + public void VerifyOnInitialized() + { + Assert.Multiple(() => + { + Assert.That(this.renderer.Instance.ViewModel, Is.EqualTo(this.viewModel.Object)); + Assert.That(this.renderer.Markup, Does.Contain(this.structure.First().Name)); + }); + } + + [Test] + public async Task VerifyEditFile() + { + var treeNodeEventMock = new Mock(); + var rootNode = this.structure[0]; + + treeNodeEventMock.Setup(x => x.DataItem).Returns(rootNode.Content.First(x => x.Thing is not File)); + this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); + Assert.That(this.renderer.Instance.IsFileFormVisibile, Is.EqualTo(false)); + + treeNodeEventMock.Setup(x => x.DataItem).Returns(rootNode.Content.First(x => x.Thing is File)); + this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); + this.renderer.Render(); + + var form = this.renderer.FindComponent(); + + Assert.Multiple(() => + { + Assert.That(form, Is.Not.Null); + Assert.That(form.Instance.ShouldCreate, Is.EqualTo(false)); + Assert.That(this.renderer.Instance.IsFileFormVisibile, Is.EqualTo(true)); + Assert.That(this.renderer.Instance.IsFolderFormVisibile, Is.EqualTo(false)); + }); + + var popup = this.renderer.FindComponent(); + await this.renderer.InvokeAsync(popup.Instance.Closed.InvokeAsync); + + Assert.Multiple(() => + { + Assert.That(this.renderer.Instance.IsFileFormVisibile, Is.EqualTo(false)); + Assert.That(this.renderer.Instance.IsFolderFormVisibile, Is.EqualTo(false)); + }); + } + + [Test] + public void VerifyEditFolder() + { + var rootNode = this.structure[0]; + var notFolderRow = rootNode.Content.First(x => x.Thing is not Folder); + this.renderer.Instance.OnEditFolderClick(notFolderRow); + Assert.That(this.renderer.Instance.IsFolderFormVisibile, Is.EqualTo(false)); + + var folderRow = rootNode.Content.First(x => x.Thing is Folder); + this.renderer.Instance.OnEditFolderClick(folderRow); + + this.renderer.Render(); + + var form = this.renderer.FindComponent(); + + Assert.Multiple(() => + { + Assert.That(form, Is.Not.Null); + Assert.That(form.Instance.ShouldCreate, Is.EqualTo(false)); + Assert.That(this.renderer.Instance.IsFileFormVisibile, Is.EqualTo(false)); + Assert.That(this.renderer.Instance.IsFolderFormVisibile, Is.EqualTo(true)); + }); + } + + [Test] + public async Task VerifyDragAndDrop() + { + var rootNode = this.structure.First(); + + // Test drag a file into a folder + var draggedFileNode = rootNode.Content[0]; + var draggableFile = this.renderer.FindAll("div").Where(x => x.Attributes["draggable"]?.Value == "true").ElementAt(1); + await this.renderer.InvokeAsync(() => draggableFile.DragStartAsync(new DragEventArgs())); + Assert.That(this.renderer.Instance.DraggedNode, Is.EqualTo(draggedFileNode)); + + var droppableDiv = this.renderer.FindAll("div").Where(x => x.Attributes["draggable"]?.Value == "true").ElementAt(0); + await this.renderer.InvokeAsync(() => droppableDiv.DropAsync(new DragEventArgs())); + this.fileHandlerViewModel.Verify(x => x.MoveFile((File)draggedFileNode.Thing, (Folder)rootNode.Thing)); + + // Test drag a folder into a folder + var draggedFolderNode = rootNode.Content[1]; + var draggableFolder = this.renderer.FindAll("div").Where(x => x.Attributes["draggable"]?.Value == "true").ElementAt(2); + await this.renderer.InvokeAsync(() => draggableFolder.DragStartAsync(new DragEventArgs())); + Assert.That(this.renderer.Instance.DraggedNode, Is.EqualTo(draggedFolderNode)); + + droppableDiv = this.renderer.FindAll("div").Where(x => x.Attributes["draggable"]?.Value == "true").ElementAt(0); + await this.renderer.InvokeAsync(() => droppableDiv.DropAsync(new DragEventArgs())); + this.folderHandlerViewModel.Verify(x => x.MoveFolder((Folder)draggedFolderNode.Thing, (Folder)rootNode.Thing)); + + // Test drop something in a file - invalid + var invalidDroppableDiv = this.renderer.FindAll("div").Where(x => x.Attributes["draggable"]?.Value == "true").ElementAt(1); + await this.renderer.InvokeAsync(() => invalidDroppableDiv.DropAsync(new DragEventArgs())); + this.fileHandlerViewModel.Verify(x => x.MoveFile((File)draggedFileNode.Thing, (Folder)rootNode.Thing)); + } + } +} diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFormTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFormTestFixture.cs new file mode 100644 index 00000000..fc0a6eb9 --- /dev/null +++ b/COMETwebapp.Tests/Components/EngineeringModel/FileStore/FolderFormTestFixture.cs @@ -0,0 +1,100 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Components.EngineeringModel.FileStore +{ + using System.Linq; + + using Bunit; + + using CDP4Common.EngineeringModelData; + + using COMET.Web.Common.Test.Helpers; + + using COMETwebapp.Components.EngineeringModel.FileStore; + using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components.Forms; + + using Moq; + + using NUnit.Framework; + + using TestContext = Bunit.TestContext; + + [TestFixture] + public class FolderFormTestFixture + { + private TestContext context; + private IRenderedComponent renderer; + private Mock viewModel; + + [SetUp] + public void SetUp() + { + this.context = new TestContext(); + this.viewModel = new Mock(); + + this.viewModel.Setup(x => x.Folder).Returns(new Folder()); + this.context.ConfigureDevExpressBlazor(); + + this.renderer = this.context.RenderComponent(parameters => + { + parameters.Add(p => p.ViewModel, this.viewModel.Object); + }); + } + + [TearDown] + public void Teardown() + { + this.context.CleanContext(); + this.context.Dispose(); + } + + [Test] + public async Task VerifyOnValidSubmitAndDelete() + { + Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(false)); + + var form = this.renderer.FindComponent(); + await this.renderer.InvokeAsync(form.Instance.OnValidSubmit.InvokeAsync); + this.viewModel.Verify(x => x.CreateOrEditFolder(It.IsAny()), Times.Once); + + var deleteFolderButton = this.renderer.FindComponents().First(x => x.Instance.Id == "deleteFolderButton"); + await this.renderer.InvokeAsync(deleteFolderButton.Instance.Click.InvokeAsync); + Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(true)); + + var deleteFolderPopupButton = this.renderer.FindComponents().First(x => x.Instance.Id == "deleteFolderPopupButton"); + await this.renderer.InvokeAsync(deleteFolderPopupButton.Instance.Click.InvokeAsync); + + Assert.Multiple(() => + { + Assert.That(this.renderer.Instance.IsDeletePopupVisible, Is.EqualTo(false)); + this.viewModel.Verify(x => x.DeleteFolder(), Times.Once); + }); + } + } +} diff --git a/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs b/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs deleted file mode 100644 index 4a813c62..00000000 --- a/COMETwebapp.Tests/Components/EngineeringModel/FolderFileStructureTestFixture.cs +++ /dev/null @@ -1,167 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2023-2024 RHEA System S.A. -// -// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua -// -// This file is part of CDP4-COMET WEB Community Edition -// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Components.EngineeringModel -{ - using System.Linq; - using System.Threading.Tasks; - - using Bunit; - - using CDP4Common.EngineeringModelData; - - using COMET.Web.Common.Test.Helpers; - - using COMETwebapp.Components.EngineeringModel.FileStore; - using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore; - - using DevExpress.Blazor; - - using Microsoft.AspNetCore.Components.Forms; - - using Moq; - - using NUnit.Framework; - - using TestContext = Bunit.TestContext; - - [TestFixture] - public class FolderFileStructureTestFixture - { - private TestContext context; - private IRenderedComponent renderer; - private Mock viewModel; - private List structure; - - /* [SetUp] - public void SetUp() - { - this.context = new TestContext(); - this.viewModel = new Mock(); - - var file = new File(); - - file.FileRevision.Add(new FileRevision() - { - Name = "File Revision 1" - }); - - this.structure = - [ - new FileFolderNodeViewModel(file), - new FileFolderNodeViewModel(new Folder(), [new FileFolderNodeViewModel(file)]), - ]; - - this.viewModel.Setup(x => x.File).Returns(new File()); - this.viewModel.Setup(x => x.Folder).Returns(new Folder()); - this.viewModel.Setup(x => x.Structure).Returns(this.structure); - - this.context.ConfigureDevExpressBlazor(); - - this.renderer = this.context.RenderComponent(parameters => - { - parameters.Add(p => p.ViewModel, this.viewModel.Object); - }); - } - - [TearDown] - public void Teardown() - { - this.context.CleanContext(); - this.context.Dispose(); - } - - [Test] - public void VerifyOnInitialized() - { - Assert.Multiple(() => - { - Assert.That(this.renderer.Instance.ViewModel, Is.EqualTo(this.viewModel.Object)); - Assert.That(this.renderer.Markup, Does.Contain(this.structure.First().Name)); - }); - } - - [Test] - public async Task VerifyEditFile() - { - var treeNodeEventMock = new Mock(); - treeNodeEventMock.Setup(x => x.DataItem).Returns(this.structure.First(x => x.Thing is not File)); - this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); - Assert.That(this.renderer.Instance.SelectedFile, Is.Null); - - treeNodeEventMock.Setup(x => x.DataItem).Returns(this.structure.First(x => x.Thing is File)); - this.renderer.Instance.OnNodeClick(treeNodeEventMock.Object); - this.renderer.Render(); - - var form = this.renderer.FindComponent(); - - Assert.Multiple(() => - { - Assert.That(form, Is.Not.Null); - Assert.That(form.Instance.ShouldCreate, Is.EqualTo(false)); - Assert.That(this.renderer.Instance.SelectedFile, Is.Not.Null); - Assert.That(this.renderer.Instance.SelectedFolder, Is.Null); - }); - - // Test behavior in case the submmit is valid => next step - var editForm = form.FindComponent(); - await form.InvokeAsync(editForm.Instance.OnValidSubmit.InvokeAsync); - - var cancelButton = editForm.FindComponents().First(x => x.Instance.Id == "cancelFileButton"); - await editForm.InvokeAsync(cancelButton.Instance.Click.InvokeAsync); - Assert.That(this.renderer.Instance.SelectedFile, Is.Null); - } - - [Test] - public async Task VerifyEditFolder() - { - var notFolderRow = this.structure.First(x => x.Thing is not Folder); - this.renderer.Instance.OnEditFolderClick(notFolderRow); - Assert.That(this.renderer.Instance.SelectedFolder, Is.Null); - - var folderRow = this.structure.First(x => x.Thing is Folder); - this.renderer.Instance.OnEditFolderClick(folderRow); - - this.renderer.Render(); - - var form = this.renderer.FindComponent(); - - Assert.Multiple(() => - { - Assert.That(form, Is.Not.Null); - Assert.That(form.Instance.ShouldCreate, Is.EqualTo(false)); - Assert.That(this.renderer.Instance.SelectedFile, Is.Null); - Assert.That(this.renderer.Instance.SelectedFolder, Is.Not.Null); - }); - - // Test behavior in case the submmit is valid => next step - var editForm = form.FindComponent(); - await form.InvokeAsync(editForm.Instance.OnValidSubmit.InvokeAsync); - - var cancelButton = editForm.FindComponents().First(x => x.Instance.Id == "cancelFolderButton"); - await editForm.InvokeAsync(cancelButton.Instance.Click.InvokeAsync); - Assert.That(this.renderer.Instance.SelectedFolder, Is.Null); - }*/ - } -} diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor index dd07e969..cbb25fb2 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor @@ -79,6 +79,6 @@ Copyright (c) 2023-2024 RHEA System S.A. You are about to delete the File: @(this.ViewModel.File.CurrentFileRevision?.Name)
- +
diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor.cs index 706a4ced..33dcd675 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileForm.razor.cs @@ -43,9 +43,9 @@ public partial class FileForm : SelectedDataItemForm public IFileHandlerViewModel ViewModel { get; set; } /// - /// Gets or sets the value to check if the deletion popup is visible + /// Gets the value to check if the deletion popup is visible /// - private bool IsDeletePopupVisible { get; set; } + public bool IsDeletePopupVisible { get; private set; } /// /// Method that is executed when there is a valid submit diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor index d74baf22..8e94dfe7 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FileTypesTable.razor @@ -48,7 +48,7 @@ Copyright (c) 2023-2024 RHEA System S.A. IconCssClass="oi oi-arrow-bottom" Click="() => this.MoveDown(row)" Enabled="@(this.SelectedFileTypes.LastOrDefault() != row.Thing)" /> - } diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor b/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor index c2b06cfa..b74e5afe 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor @@ -71,6 +71,6 @@ Copyright (c) 2023-2024 RHEA System S.A. You are about to delete the Folder: @(this.ViewModel.Folder.Name)
- +
diff --git a/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor.cs b/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor.cs index f3eecfcc..31f2cc9a 100644 --- a/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor.cs +++ b/COMETwebapp/Components/EngineeringModel/FileStore/FolderForm.razor.cs @@ -45,9 +45,9 @@ public partial class FolderForm : SelectedDataItemForm public IFolderHandlerViewModel ViewModel { get; set; } /// - /// Gets or sets the value to check if the deletion popup is visible + /// Gets the value to check if the deletion popup is visible /// - private bool IsDeletePopupVisible { get; set; } + public bool IsDeletePopupVisible { get; private set; } /// /// Method that is executed when there is a valid submit diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs index f35a1354..aeac7abb 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs @@ -83,8 +83,6 @@ public FileRevisionHandlerViewModel(ISessionService sessionService, ICDPMessageB this.JsUtilitiesService = jsUtilitiesService; this.logger = logger; this.configuration = configuration; - - this.InitializeSubscriptions([typeof(File), typeof(Folder)]); } /// From 9f5b085a0a724ba54259f87f1aec9c5160aa966e Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Tue, 16 Apr 2024 17:33:14 +0100 Subject: [PATCH 09/12] issues fixed --- .../SessionServiceTestFixture.cs | 2 +- .../SessionManagement/ISessionService.cs | 18 ++--- .../SessionManagement/SessionService.cs | 70 +++++++++---------- .../Interoperability/JsUtilitiesService.cs | 24 ++++++- .../FileHandler/FileHandlerViewModel.cs | 2 + COMETwebapp/wwwroot/Scripts/utilities.js | 2 +- 6 files changed, 70 insertions(+), 48 deletions(-) diff --git a/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs b/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs index f164ea3c..dc6df73d 100644 --- a/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs +++ b/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs @@ -326,7 +326,7 @@ public void VerifySwitchDomain() } [Test] - public async Task VerifyUpdateThings() + public void VerifyUpdateThings() { var thingsToUpdate = new List(); this.sessionService.IsSessionOpen = true; diff --git a/COMET.Web.Common/Services/SessionManagement/ISessionService.cs b/COMET.Web.Common/Services/SessionManagement/ISessionService.cs index 0a85e236..8c398dc6 100644 --- a/COMET.Web.Common/Services/SessionManagement/ISessionService.cs +++ b/COMET.Web.Common/Services/SessionManagement/ISessionService.cs @@ -170,6 +170,15 @@ public interface ISessionService /// An asynchronous operation with a Task UpdateThings(Thing container, IEnumerable thingsToUpdate); + /// + /// Write updated Things in an and uploads the given files to the filestore + /// + /// The where the s should be updated + /// List of Things to update in the session + /// >A collection of file paths for files to be send to the file store + /// An asynchronous operation with a + Task UpdateThings(Thing container, IEnumerable thingsToUpdate, IEnumerable files); + /// /// Deletes a from it's container /// @@ -239,14 +248,5 @@ public interface ISessionService /// in. /// Task ReadEngineeringModels(IEnumerable engineeringModelSetups); - - /// - /// Write updated Things in an and uploads the given files to the filestore - /// - /// The where the s should be updated - /// List of Things to update in the session - /// >A collection of file paths for files to be send to the file store - /// An asynchronous operation with a - Task UpdateThings(Thing container, IEnumerable thingsToUpdate, IEnumerable files); } } diff --git a/COMET.Web.Common/Services/SessionManagement/SessionService.cs b/COMET.Web.Common/Services/SessionManagement/SessionService.cs index 724cf05c..21981873 100644 --- a/COMET.Web.Common/Services/SessionManagement/SessionService.cs +++ b/COMET.Web.Common/Services/SessionManagement/SessionService.cs @@ -319,13 +319,34 @@ public async Task CreateThings(Thing container, IEnumerable thing } /// - /// Write updated Things in an and uploads the given files to the filestore + /// Write updated Thing in an + /// + /// The where the s should be updated + /// the thing to update in the session + /// An asynchronous operation with a + public Task UpdateThing(Thing container, Thing thingToUpdate) + { + return this.UpdateThings(container, new List { thingToUpdate }); + } + + /// + /// Write updated Things in an /// /// The where the s should be updated /// List of Things to update in the session - /// >A collection of file paths for files to be send to the file store /// An asynchronous operation with a - public async Task UpdateThings(Thing container, IEnumerable thingsToUpdate, IEnumerable files) + public Task UpdateThings(Thing container, params Thing[] thingsToUpdate) + { + return this.UpdateThings(container, thingsToUpdate.ToList()); + } + + /// + /// Write updated Things in an + /// + /// The where the s should be updated + /// List of Things to update in the session + /// An asynchronous operation with a + public async Task UpdateThings(Thing container, IEnumerable thingsToUpdate) { var result = new Result(); @@ -352,17 +373,17 @@ public async Task UpdateThings(Thing container, IEnumerable thing // register all updates with the transaction. thingsToUpdate.ToList().ForEach(transaction.CreateOrUpdate); - // finalize the transaction, the result is an OperationContainer that the session class uses to write the changes to the Thing object. + // finalize the transaction, the result is an OperationContainer that the session class uses to write the changes + // to the Thing object. var operationContainer = transaction.FinalizeTransaction(); - result = new Result(); try { - await this.Session.Write(operationContainer, files); + await this.Session.Write(operationContainer); this.logger.LogInformation("Update writing done in {swElapsedMilliseconds} [ms]", sw.ElapsedMilliseconds); result.Successes.Add(new Success($"Update writing done in {sw.ElapsedMilliseconds} [ms]")); } - catch (DalWriteException ex) + catch (Exception ex) { this.logger.LogError("The update operation failed: {exMessage}", ex.Message); result.Errors.Add(new Error($"The update operation failed: {ex.Message}")); @@ -376,34 +397,13 @@ public async Task UpdateThings(Thing container, IEnumerable thing } /// - /// Write updated Thing in an - /// - /// The where the s should be updated - /// the thing to update in the session - /// An asynchronous operation with a - public Task UpdateThing(Thing container, Thing thingToUpdate) - { - return this.UpdateThings(container, new List { thingToUpdate }); - } - - /// - /// Write updated Things in an - /// - /// The where the s should be updated - /// List of Things to update in the session - /// An asynchronous operation with a - public Task UpdateThings(Thing container, params Thing[] thingsToUpdate) - { - return this.UpdateThings(container, thingsToUpdate.ToList()); - } - - /// - /// Write updated Things in an + /// Write updated Things in an and uploads the given files to the filestore /// /// The where the s should be updated /// List of Things to update in the session + /// >A collection of file paths for files to be send to the file store /// An asynchronous operation with a - public async Task UpdateThings(Thing container, IEnumerable thingsToUpdate) + public async Task UpdateThings(Thing container, IEnumerable thingsToUpdate, IEnumerable files) { var result = new Result(); @@ -430,17 +430,17 @@ public async Task UpdateThings(Thing container, IEnumerable thing // register all updates with the transaction. thingsToUpdate.ToList().ForEach(transaction.CreateOrUpdate); - // finalize the transaction, the result is an OperationContainer that the session class uses to write the changes - // to the Thing object. + // finalize the transaction, the result is an OperationContainer that the session class uses to write the changes to the Thing object. var operationContainer = transaction.FinalizeTransaction(); + result = new Result(); try { - await this.Session.Write(operationContainer); + await this.Session.Write(operationContainer, files); this.logger.LogInformation("Update writing done in {swElapsedMilliseconds} [ms]", sw.ElapsedMilliseconds); result.Successes.Add(new Success($"Update writing done in {sw.ElapsedMilliseconds} [ms]")); } - catch (Exception ex) + catch (DalWriteException ex) { this.logger.LogError("The update operation failed: {exMessage}", ex.Message); result.Errors.Add(new Error($"The update operation failed: {ex.Message}")); diff --git a/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs b/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs index 93c2e127..301a2270 100644 --- a/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs +++ b/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs @@ -47,11 +47,31 @@ public JsUtilitiesService(IJSRuntime jsRuntime) : base(jsRuntime) /// an asynchronous operation public async Task DownloadFileFromStreamAsync(Stream stream, string fileName) { - ArgumentNullException.ThrowIfNull(stream); - ArgumentNullException.ThrowIfNull(fileName); + ValidateDownloadParameters(stream, fileName); using var streamRef = new DotNetStreamReference(stream: stream); + await this.DownloadFileAsync(fileName, streamRef); + } + + /// + /// Validates the download file parameters + /// + /// the stream + /// the file name + private static void ValidateDownloadParameters(Stream stream, string fileName) + { + ArgumentNullException.ThrowIfNull(stream, nameof(stream)); + ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + } + /// + /// Effectively downloads the file + /// + /// the file name + /// the stream + /// A + private async Task DownloadFileAsync(string fileName, DotNetStreamReference streamRef) + { await this.JsRuntime.InvokeVoidAsync("DownloadFileFromStream", fileName, streamRef); } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs index 6ee8e7c2..510a2824 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileHandler/FileHandlerViewModel.cs @@ -165,6 +165,7 @@ public async Task MoveFile(File file, Folder targetFolder) public async Task CreateOrEditFile(bool shouldCreate) { this.IsLoading = true; + this.logger.LogInformation("Creating or editing file"); var thingsToUpdate = new List(); var fileStoreClone = this.CurrentFileStore.Clone(true); @@ -208,6 +209,7 @@ public async Task CreateOrEditFile(bool shouldCreate) await this.SessionService.UpdateThings(fileStoreClone, thingsToUpdate, newFileRevisions.Select(x => x.LocalPath)); await this.SessionService.RefreshSession(); + this.logger.LogInformation("File with iid {iid} updated successfully", this.File.Iid); this.IsLoading = false; } diff --git a/COMETwebapp/wwwroot/Scripts/utilities.js b/COMETwebapp/wwwroot/Scripts/utilities.js index c9babf72..1d020915 100644 --- a/COMETwebapp/wwwroot/Scripts/utilities.js +++ b/COMETwebapp/wwwroot/Scripts/utilities.js @@ -3,7 +3,7 @@ * @param {string} fileName * @param {any} contentStreamReference */ -var DownloadFileFromStream = async (fileName, contentStreamReference) => { +const DownloadFileFromStream = async (fileName, contentStreamReference) => { const arrayBuffer = await contentStreamReference.arrayBuffer(); const blob = new Blob([arrayBuffer], { type: 'text/plain;charset=utf-8' }); From 6e6ef7d18fbe690c62dd733d26465d4b00c3c188 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Tue, 16 Apr 2024 17:53:34 +0100 Subject: [PATCH 10/12] fixed new issues + tests for validators --- .../CommonFileStoreValidatorTestFixture.cs | 62 ++++++++++++++++++ .../FileRevisionValidatorTestFixture.cs | 64 +++++++++++++++++++ .../FileTypeValidatorTestFixture.cs | 62 ++++++++++++++++++ .../FileValidatorTestFixture.cs | 62 ++++++++++++++++++ .../FolderValidatorTestFixture.cs | 63 ++++++++++++++++++ .../Interoperability/JsUtilitiesService.cs | 4 +- .../EngineeringModel/FileValidator.cs | 2 +- COMETwebapp/wwwroot/Scripts/utilities.js | 27 ++++---- 8 files changed, 330 insertions(+), 16 deletions(-) create mode 100644 COMETwebapp.Tests/Validators/EngineeringModel/CommonFileStoreValidatorTestFixture.cs create mode 100644 COMETwebapp.Tests/Validators/EngineeringModel/FileRevisionValidatorTestFixture.cs create mode 100644 COMETwebapp.Tests/Validators/EngineeringModel/FileTypeValidatorTestFixture.cs create mode 100644 COMETwebapp.Tests/Validators/EngineeringModel/FileValidatorTestFixture.cs create mode 100644 COMETwebapp.Tests/Validators/EngineeringModel/FolderValidatorTestFixture.cs diff --git a/COMETwebapp.Tests/Validators/EngineeringModel/CommonFileStoreValidatorTestFixture.cs b/COMETwebapp.Tests/Validators/EngineeringModel/CommonFileStoreValidatorTestFixture.cs new file mode 100644 index 00000000..59a2793f --- /dev/null +++ b/COMETwebapp.Tests/Validators/EngineeringModel/CommonFileStoreValidatorTestFixture.cs @@ -0,0 +1,62 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + using CDP4Common.Validation; + + using COMETwebapp.Validators.EngineeringModel; + + using NUnit.Framework; + + [TestFixture] + public class CommonFileStoreValidatorTestFixture + { + private CommonFileStoreValidator validator; + + [SetUp] + public void SetUp() + { + var validationService = new ValidationService(); + this.validator = new CommonFileStoreValidator(validationService); + } + + [Test] + public void VerifyValidationScenarios() + { + var commonFileStore = new CommonFileStore(); + Assert.That(this.validator.Validate(commonFileStore).IsValid, Is.EqualTo(false)); + + commonFileStore = new CommonFileStore() + { + Name = "name1", + Owner = new DomainOfExpertise() + }; + + Assert.That(this.validator.Validate(commonFileStore).IsValid, Is.EqualTo(true)); + } + } +} diff --git a/COMETwebapp.Tests/Validators/EngineeringModel/FileRevisionValidatorTestFixture.cs b/COMETwebapp.Tests/Validators/EngineeringModel/FileRevisionValidatorTestFixture.cs new file mode 100644 index 00000000..1b9ac743 --- /dev/null +++ b/COMETwebapp.Tests/Validators/EngineeringModel/FileRevisionValidatorTestFixture.cs @@ -0,0 +1,64 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + using CDP4Common.Validation; + + using COMETwebapp.Validators.EngineeringModel; + + using NUnit.Framework; + + [TestFixture] + public class FileRevisionValidatorTestFixture + { + private FileRevisionValidator validator; + + [SetUp] + public void SetUp() + { + var validationService = new ValidationService(); + this.validator = new FileRevisionValidator(validationService); + } + + [Test] + public void VerifyValidationScenarios() + { + var fileRevision = new FileRevision(); + Assert.That(this.validator.Validate(fileRevision).IsValid, Is.EqualTo(false)); + + fileRevision = new FileRevision() + { + Name = "name1", + LocalPath = "/path" + }; + + Assert.That(this.validator.Validate(fileRevision).IsValid, Is.EqualTo(false)); + fileRevision.FileType.Add(new FileType()); + Assert.That(this.validator.Validate(fileRevision).IsValid, Is.EqualTo(true)); + } + } +} diff --git a/COMETwebapp.Tests/Validators/EngineeringModel/FileTypeValidatorTestFixture.cs b/COMETwebapp.Tests/Validators/EngineeringModel/FileTypeValidatorTestFixture.cs new file mode 100644 index 00000000..4fdba8d5 --- /dev/null +++ b/COMETwebapp.Tests/Validators/EngineeringModel/FileTypeValidatorTestFixture.cs @@ -0,0 +1,62 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Validators.EngineeringModel +{ + using CDP4Common.SiteDirectoryData; + using CDP4Common.Validation; + + using COMETwebapp.Validators.EngineeringModel; + + using NUnit.Framework; + + [TestFixture] + public class FileTypeValidatorTestFixture + { + private FileTypeValidator validator; + + [SetUp] + public void SetUp() + { + var validationService = new ValidationService(); + this.validator = new FileTypeValidator(validationService); + } + + [Test] + public void VerifyValidationScenarios() + { + var fileType = new FileType(); + Assert.That(this.validator.Validate(fileType).IsValid, Is.EqualTo(false)); + + fileType = new FileType() + { + Name = "name1", + }; + + Assert.That(this.validator.Validate(fileType).IsValid, Is.EqualTo(false)); + fileType.Extension = "ext"; + Assert.That(this.validator.Validate(fileType).IsValid, Is.EqualTo(true)); + } + } +} diff --git a/COMETwebapp.Tests/Validators/EngineeringModel/FileValidatorTestFixture.cs b/COMETwebapp.Tests/Validators/EngineeringModel/FileValidatorTestFixture.cs new file mode 100644 index 00000000..9a58910f --- /dev/null +++ b/COMETwebapp.Tests/Validators/EngineeringModel/FileValidatorTestFixture.cs @@ -0,0 +1,62 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + using CDP4Common.Validation; + + using COMETwebapp.Validators.EngineeringModel; + + using NUnit.Framework; + + [TestFixture] + public class FileValidatorTestFixture + { + private FileValidator validator; + + [SetUp] + public void SetUp() + { + var validationService = new ValidationService(); + this.validator = new FileValidator(validationService); + } + + [Test] + public void VerifyValidationScenarios() + { + var file = new File(); + Assert.That(this.validator.Validate(file).IsValid, Is.EqualTo(false)); + + file = new File() + { + Owner = new DomainOfExpertise(), + LockedBy = null + }; + + Assert.That(this.validator.Validate(file).IsValid, Is.EqualTo(true)); + } + } +} diff --git a/COMETwebapp.Tests/Validators/EngineeringModel/FolderValidatorTestFixture.cs b/COMETwebapp.Tests/Validators/EngineeringModel/FolderValidatorTestFixture.cs new file mode 100644 index 00000000..6311fff5 --- /dev/null +++ b/COMETwebapp.Tests/Validators/EngineeringModel/FolderValidatorTestFixture.cs @@ -0,0 +1,63 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 RHEA System S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Antoine Théate, João Rua +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the RHEA 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 COMETwebapp.Tests.Validators.EngineeringModel +{ + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + using CDP4Common.Validation; + + using COMETwebapp.Validators.EngineeringModel; + + using NUnit.Framework; + + [TestFixture] + public class FolderValidatorTestFixture + { + private FolderValidator validator; + + [SetUp] + public void SetUp() + { + var validationService = new ValidationService(); + this.validator = new FolderValidator(validationService); + } + + [Test] + public void VerifyValidationScenarios() + { + var folder = new Folder(); + Assert.That(this.validator.Validate(folder).IsValid, Is.EqualTo(false)); + + folder = new Folder() + { + Name = "folder A", + Owner = new DomainOfExpertise(), + ContainingFolder = null + }; + + Assert.That(this.validator.Validate(folder).IsValid, Is.EqualTo(true)); + } + } +} diff --git a/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs b/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs index 301a2270..ab506e24 100644 --- a/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs +++ b/COMETwebapp/Services/Interoperability/JsUtilitiesService.cs @@ -60,8 +60,8 @@ public async Task DownloadFileFromStreamAsync(Stream stream, string fileName) /// the file name private static void ValidateDownloadParameters(Stream stream, string fileName) { - ArgumentNullException.ThrowIfNull(stream, nameof(stream)); - ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(fileName); } /// diff --git a/COMETwebapp/Validators/EngineeringModel/FileValidator.cs b/COMETwebapp/Validators/EngineeringModel/FileValidator.cs index 43e3b09d..02c81b18 100644 --- a/COMETwebapp/Validators/EngineeringModel/FileValidator.cs +++ b/COMETwebapp/Validators/EngineeringModel/FileValidator.cs @@ -41,7 +41,7 @@ public class FileValidator : AbstractValidator /// public FileValidator(IValidationService validationService) : base() { - this.RuleFor(x => x.Owner).Validate(validationService, nameof(File.Owner)); + this.RuleFor(x => x.Owner).NotEmpty().Validate(validationService, nameof(File.Owner)); this.RuleFor(x => x.LockedBy).Validate(validationService, nameof(File.LockedBy)); } } diff --git a/COMETwebapp/wwwroot/Scripts/utilities.js b/COMETwebapp/wwwroot/Scripts/utilities.js index 1d020915..a5214369 100644 --- a/COMETwebapp/wwwroot/Scripts/utilities.js +++ b/COMETwebapp/wwwroot/Scripts/utilities.js @@ -3,19 +3,20 @@ * @param {string} fileName * @param {any} contentStreamReference */ -const DownloadFileFromStream = async (fileName, contentStreamReference) => { +function DownloadFileFromStream(fileName, contentStreamReference) { + (async () => { + const arrayBuffer = await contentStreamReference.arrayBuffer(); + const blob = new Blob([arrayBuffer], { type: 'text/plain;charset=utf-8' }); - const arrayBuffer = await contentStreamReference.arrayBuffer(); - const blob = new Blob([arrayBuffer], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); - const url = URL.createObjectURL(blob); + const anchorElement = document.createElement('a'); - const anchorElement = document.createElement('a'); - - anchorElement.href = url; - anchorElement.download = fileName ?? ''; - anchorElement.target = "_blank"; - anchorElement.click(); - anchorElement.remove(); - URL.revokeObjectURL(url); -} + anchorElement.href = url; + anchorElement.download = fileName ?? ''; + anchorElement.target = "_blank"; + anchorElement.click(); + anchorElement.remove(); + URL.revokeObjectURL(url); + })(); +} \ No newline at end of file From 001407325743d33cd31e1d0aba01840c81e53378 Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Wed, 17 Apr 2024 09:01:43 +0100 Subject: [PATCH 11/12] tests fixed + removed race condition in file download --- .../SessionServiceTestFixture.cs | 6 ++--- .../JsUtilitiesServiceTestFixture.cs | 9 ++++--- COMETwebapp/Extensions/FolderExtensions.cs | 3 +-- .../FileRevisionHandlerViewModel.cs | 3 +-- COMETwebapp/wwwroot/Scripts/utilities.js | 24 +++++++++---------- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs b/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs index dc6df73d..63864c32 100644 --- a/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs +++ b/COMET.Web.Common.Tests/Services/SessionManagement/SessionServiceTestFixture.cs @@ -342,18 +342,18 @@ public void VerifyUpdateThings() var clone = element.Clone(false); clone.Name = "Satellite"; thingsToUpdate.Add(clone); - Assert.DoesNotThrowAsync(async() => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate)); + Assert.That(async () => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate), Throws.Nothing); var filesToUpload = new List { this.uri.LocalPath }; Assert.Multiple(() => { - Assert.DoesNotThrowAsync(async () => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate, filesToUpload)); + Assert.That(async () => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate, filesToUpload), Throws.Nothing); this.session.Verify(x => x.Write(It.IsAny(), It.IsAny>()), Times.Once); }); this.session.Setup(x => x.Write(It.IsAny(), It.IsAny>())).Throws(new DalWriteException()); - Assert.DoesNotThrowAsync(async () => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate, filesToUpload)); + Assert.That(async () => await this.sessionService.UpdateThings(this.iteration, thingsToUpdate, filesToUpload), Throws.Nothing); } [Test] diff --git a/COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs b/COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs index f49fefef..e453c299 100644 --- a/COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs +++ b/COMETwebapp.Tests/Services/Interoperability/JsUtilitiesServiceTestFixture.cs @@ -48,9 +48,12 @@ public void SetUp() [Test] public async Task VerifyFileDownloadUtility() { - Assert.ThrowsAsync(async () => await this.service.DownloadFileFromStreamAsync(null, null)); - Assert.ThrowsAsync(async () => await this.service.DownloadFileFromStreamAsync(new MemoryStream(), null)); - + Assert.Multiple(() => + { + Assert.That(async () => await this.service.DownloadFileFromStreamAsync(null, null), Throws.ArgumentNullException); + Assert.That(async () => await this.service.DownloadFileFromStreamAsync(new MemoryStream(), null), Throws.ArgumentNullException); + }); + await this.service.DownloadFileFromStreamAsync(new MemoryStream(), "fileTest"); Assert.That(this.jsRuntimeMock.Invocations.Count, Is.EqualTo(1)); } diff --git a/COMETwebapp/Extensions/FolderExtensions.cs b/COMETwebapp/Extensions/FolderExtensions.cs index 2bfcf83f..ff9c5c6e 100644 --- a/COMETwebapp/Extensions/FolderExtensions.cs +++ b/COMETwebapp/Extensions/FolderExtensions.cs @@ -38,8 +38,7 @@ public static class FolderExtensions /// The path public static string GetFolderPath(this Folder folder) { - var path = $"{folder.Path}/{folder.Name}/"; - return path; + return Path.Combine(folder.Path, folder.Name); } } } diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs index aeac7abb..04eafd7b 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs @@ -126,8 +126,7 @@ public void InitializeViewModel(File file, FileStore fileStore) this.CurrentFileStore = fileStore; this.FileTypes = this.SessionService.GetSiteDirectory().AvailableReferenceDataLibraries().SelectMany(x => x.FileType); this.ErrorMessage = string.Empty; - - this.UploadsDirectory = $"wwwroot/uploads/{Guid.NewGuid()}"; + this.UploadsDirectory = Path.Combine("wwwroot", "uploads", Guid.NewGuid().ToString()); if (!Directory.Exists(this.UploadsDirectory)) { diff --git a/COMETwebapp/wwwroot/Scripts/utilities.js b/COMETwebapp/wwwroot/Scripts/utilities.js index a5214369..7dd68a94 100644 --- a/COMETwebapp/wwwroot/Scripts/utilities.js +++ b/COMETwebapp/wwwroot/Scripts/utilities.js @@ -3,20 +3,18 @@ * @param {string} fileName * @param {any} contentStreamReference */ -function DownloadFileFromStream(fileName, contentStreamReference) { - (async () => { - const arrayBuffer = await contentStreamReference.arrayBuffer(); - const blob = new Blob([arrayBuffer], { type: 'text/plain;charset=utf-8' }); +async function DownloadFileFromStream(fileName, contentStreamReference) { + const arrayBuffer = await contentStreamReference.arrayBuffer(); + const blob = new Blob([arrayBuffer], { type: 'text/plain;charset=utf-8' }); - const url = URL.createObjectURL(blob); + const url = URL.createObjectURL(blob); - const anchorElement = document.createElement('a'); + const anchorElement = document.createElement('a'); - anchorElement.href = url; - anchorElement.download = fileName ?? ''; - anchorElement.target = "_blank"; - anchorElement.click(); - anchorElement.remove(); - URL.revokeObjectURL(url); - })(); + anchorElement.href = url; + anchorElement.download = fileName ?? ''; + anchorElement.target = "_blank"; + anchorElement.click(); + anchorElement.remove(); + URL.revokeObjectURL(url); } \ No newline at end of file From 0c3c09824e037fc4513ec85b56c04039f4a77d2e Mon Sep 17 00:00:00 2001 From: Joao Rua Date: Wed, 17 Apr 2024 09:07:31 +0100 Subject: [PATCH 12/12] only creates a folder with uploaded data when a file is uploaded --- .../FileRevisionHandlerViewModel.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs index 04eafd7b..0019b3a7 100644 --- a/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs +++ b/COMETwebapp/ViewModels/Components/EngineeringModel/FileStore/FileRevisionHandler/FileRevisionHandlerViewModel.cs @@ -127,11 +127,6 @@ public void InitializeViewModel(File file, FileStore fileStore) this.FileTypes = this.SessionService.GetSiteDirectory().AvailableReferenceDataLibraries().SelectMany(x => x.FileType); this.ErrorMessage = string.Empty; this.UploadsDirectory = Path.Combine("wwwroot", "uploads", Guid.NewGuid().ToString()); - - if (!Directory.Exists(this.UploadsDirectory)) - { - Directory.CreateDirectory(this.UploadsDirectory); - } } /// @@ -173,6 +168,11 @@ public async Task UploadFile(IBrowserFile file) return; } + if (!Directory.Exists(this.UploadsDirectory)) + { + Directory.CreateDirectory(this.UploadsDirectory); + } + var uploadedFilePath = Path.Combine(this.UploadsDirectory, Guid.NewGuid().ToString()); await using (var fileStream = new FileStream(uploadedFilePath, FileMode.Create))