diff --git a/Marechai.ApiClient/Models/MergeSoftwareCompilationsRequest.cs b/Marechai.ApiClient/Models/MergeSoftwareCompilationsRequest.cs
new file mode 100644
index 00000000..0af769f5
--- /dev/null
+++ b/Marechai.ApiClient/Models/MergeSoftwareCompilationsRequest.cs
@@ -0,0 +1,69 @@
+//
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using System.Collections.Generic;
+using System.IO;
+using System;
+namespace Marechai.ApiClient.Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ #pragma warning disable CS1591
+ public partial class MergeSoftwareCompilationsRequest : IAdditionalDataHolder, IParsable
+ #pragma warning restore CS1591
+ {
+ /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.
+ public IDictionary AdditionalData { get; set; }
+ /// The sourceIds property
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
+#nullable enable
+ public List? SourceIds { get; set; }
+#nullable restore
+#else
+ public List SourceIds { get; set; }
+#endif
+ /// The targetId property
+ public int? TargetId { get; set; }
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ public MergeSoftwareCompilationsRequest()
+ {
+ AdditionalData = new Dictionary();
+ }
+ ///
+ /// Creates a new instance of the appropriate class based on discriminator value
+ ///
+ /// A
+ /// The parse node to use to read the discriminator value and create the object
+ public static global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest CreateFromDiscriminatorValue(IParseNode parseNode)
+ {
+ if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode));
+ return new global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest();
+ }
+ ///
+ /// The deserialization information for the current model
+ ///
+ /// A IDictionary<string, Action<IParseNode>>
+ public virtual IDictionary> GetFieldDeserializers()
+ {
+ return new Dictionary>
+ {
+ { "sourceIds", n => { SourceIds = n.GetCollectionOfPrimitiveValues()?.AsList(); } },
+ { "targetId", n => { TargetId = n.GetIntValue(); } },
+ };
+ }
+ ///
+ /// Serializes information the current object
+ ///
+ /// Serialization writer to use to serialize this model
+ public virtual void Serialize(ISerializationWriter writer)
+ {
+ if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
+ writer.WriteCollectionOfPrimitiveValues("sourceIds", SourceIds);
+ writer.WriteIntValue("targetId", TargetId);
+ writer.WriteAdditionalData(AdditionalData);
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/Marechai.ApiClient/SoftwareCompilations/Item/Merge/MergeRequestBuilder.cs b/Marechai.ApiClient/SoftwareCompilations/Item/Merge/MergeRequestBuilder.cs
new file mode 100644
index 00000000..2dc8a207
--- /dev/null
+++ b/Marechai.ApiClient/SoftwareCompilations/Item/Merge/MergeRequestBuilder.cs
@@ -0,0 +1,102 @@
+//
+#pragma warning disable CS0618
+using Marechai.ApiClient.Models;
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace Marechai.ApiClient.SoftwareCompilations.Item.Merge
+{
+ ///
+ /// Builds and executes requests for operations under \software-compilations\{id}\merge
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class MergeRequestBuilder : BaseRequestBuilder
+ {
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public MergeRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software-compilations/{id}/merge", pathParameters)
+ {
+ }
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public MergeRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software-compilations/{id}/merge", rawUrl)
+ {
+ }
+ /// A
+ /// The request body
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ /// When receiving a 400 status code
+ /// When receiving a 401 status code
+ /// When receiving a 404 status code
+ /// When receiving a 409 status code
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
+#nullable enable
+ public async Task PostAsync(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+#nullable restore
+#else
+ public async Task PostAsync(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action> requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+#endif
+ if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
+ var requestInfo = ToPostRequestInformation(body, requestConfiguration);
+ var errorMapping = new Dictionary>
+ {
+ { "400", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue },
+ { "401", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue },
+ { "404", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue },
+ { "409", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue },
+ };
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
+ }
+ /// A
+ /// The request body
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
+#nullable enable
+ public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action>? requestConfiguration = default)
+ {
+#nullable restore
+#else
+ public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action> requestConfiguration = default)
+ {
+#endif
+ if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
+ var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "application/json, text/plain;q=0.9");
+ requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body);
+ return requestInfo;
+ }
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder(rawUrl, RequestAdapter);
+ }
+ ///
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ ///
+ [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class MergeRequestBuilderPostRequestConfiguration : RequestConfiguration
+ {
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/Marechai.ApiClient/SoftwareCompilations/Item/SoftwareCompilationsItemRequestBuilder.cs b/Marechai.ApiClient/SoftwareCompilations/Item/SoftwareCompilationsItemRequestBuilder.cs
index a128593c..b2bc2e18 100644
--- a/Marechai.ApiClient/SoftwareCompilations/Item/SoftwareCompilationsItemRequestBuilder.cs
+++ b/Marechai.ApiClient/SoftwareCompilations/Item/SoftwareCompilationsItemRequestBuilder.cs
@@ -3,6 +3,7 @@
using Marechai.ApiClient.Models;
using Marechai.ApiClient.SoftwareCompilations.Item.Compilations;
using Marechai.ApiClient.SoftwareCompilations.Item.Covers;
+using Marechai.ApiClient.SoftwareCompilations.Item.Merge;
using Marechai.ApiClient.SoftwareCompilations.Item.Releases;
using Marechai.ApiClient.SoftwareCompilations.Item.Software;
using Marechai.ApiClient.SoftwareCompilations.Item.Versions;
@@ -32,6 +33,11 @@ namespace Marechai.ApiClient.SoftwareCompilations.Item
{
get => new global::Marechai.ApiClient.SoftwareCompilations.Item.Covers.CoversRequestBuilder(PathParameters, RequestAdapter);
}
+ /// The merge property
+ public global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder Merge
+ {
+ get => new global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder(PathParameters, RequestAdapter);
+ }
/// The releases property
public global::Marechai.ApiClient.SoftwareCompilations.Item.Releases.ReleasesRequestBuilder Releases
{
diff --git a/Marechai.ApiClient/kiota-lock.json b/Marechai.ApiClient/kiota-lock.json
index b4bc256f..d42f148f 100644
--- a/Marechai.ApiClient/kiota-lock.json
+++ b/Marechai.ApiClient/kiota-lock.json
@@ -1,5 +1,5 @@
{
- "descriptionHash": "EF23CFA3E3D7BEEBEDACF0F1C162F3E65C5100A4E247529FC19A317185AC443E038F5D1F706BED6C0A696F2ABF5E1C4CECB13352C16548B9812C094FC0E068CC",
+ "descriptionHash": "0AA7E1D641D98EEE0410A0EB52E5A5C64562D47536F6BA516F5AF89FD4902F6253992E5ABA435A45DEB050C4C933C2E34B1474F7D82962B9971E337948E133B7",
"descriptionLocation": "../../../../../tmp/openapi.json",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.31.1",
diff --git a/Marechai.App/Presentation/ViewModels/Admin/AdminSoftwareCompilationsViewModel.cs b/Marechai.App/Presentation/ViewModels/Admin/AdminSoftwareCompilationsViewModel.cs
index 6a966d96..fd421d0c 100644
--- a/Marechai.App/Presentation/ViewModels/Admin/AdminSoftwareCompilationsViewModel.cs
+++ b/Marechai.App/Presentation/ViewModels/Admin/AdminSoftwareCompilationsViewModel.cs
@@ -27,6 +27,8 @@ public partial class AdminSoftwareCompilationsViewModel : ObservableObject, IReg
private CancellationTokenSource? _predecessorPickerDebounce;
private CancellationTokenSource? _includedSoftwareDebounce;
private CancellationTokenSource? _includedCompilationDebounce;
+ private CancellationTokenSource? _mergeTargetDebounce;
+ private CancellationTokenSource? _mergeSourceDebounce;
private int? _editingId;
private List? _allMachines;
@@ -91,6 +93,18 @@ public partial class AdminSoftwareCompilationsViewModel : ObservableObject, IReg
[ObservableProperty] private ObservableCollection _includedCompilationSuggestions = [];
[ObservableProperty] private SoftwareCompilationDto? _selectedIncludedCompilation;
+ // --- Merge ---
+ [ObservableProperty] private bool _isMerging;
+ [ObservableProperty] private string _mergeTargetSearchText = string.Empty;
+ [ObservableProperty] private ObservableCollection _mergeTargetSuggestions = [];
+ [ObservableProperty] private SoftwareCompilationDto? _selectedMergeTargetCompilation;
+ [ObservableProperty] private string _mergeSourceSearchText = string.Empty;
+ [ObservableProperty] private ObservableCollection _mergeSourceSuggestions = [];
+ [ObservableProperty] private SoftwareCompilationDto? _mergeSourcePickerValue;
+ [ObservableProperty] private ObservableCollection _mergeSelectedSources = [];
+
+ public bool CanConfirmMerge => SelectedMergeTargetCompilation?.Id is not null && MergeSelectedSources.Count > 0;
+
public AdminSoftwareCompilationsViewModel(SoftwareCompilationsService service,
IJwtService jwtService,
ITokenService tokenService,
@@ -128,6 +142,11 @@ public partial class AdminSoftwareCompilationsViewModel : ObservableObject, IReg
AddIncludedCompilationCommand = new AsyncRelayCommand(AddIncludedCompilationAsync);
RemoveIncludedCompilationCommand = new AsyncRelayCommand(RemoveIncludedCompilationAsync);
+ OpenMergeCommand = new RelayCommand(OpenMerge);
+ ConfirmMergeCommand = new AsyncRelayCommand(ConfirmMergeAsync);
+ CancelMergeCommand = new RelayCommand(CancelMerge);
+ RemoveMergeSourceCommand = new RelayCommand(RemoveMergeSource);
+
CheckAdminRole();
}
@@ -147,6 +166,11 @@ public partial class AdminSoftwareCompilationsViewModel : ObservableObject, IReg
public IAsyncRelayCommand AddIncludedCompilationCommand { get; }
public IAsyncRelayCommand RemoveIncludedCompilationCommand { get; }
+ public IRelayCommand OpenMergeCommand { get; }
+ public IAsyncRelayCommand ConfirmMergeCommand { get; }
+ public IRelayCommand CancelMergeCommand { get; }
+ public IRelayCommand RemoveMergeSourceCommand { get; }
+
// --- IRegionAware ---
public bool IsNavigationTarget(NavigationContext navigationContext) => true;
@@ -617,4 +641,123 @@ public partial class AdminSoftwareCompilationsViewModel : ObservableObject, IReg
}
private static string? NullIfEmpty(string value) => string.IsNullOrWhiteSpace(value) ? null : value;
+
+ // --- Merge ---
+
+ public string MergeConfirmDialogTitle => _localizer["MergeConfirmDialogTitle"];
+ public string MergeConfirmDialogMessage => _localizer["MergeConfirmDialogMessage"];
+ public string MergeButtonText => _localizer["MergeButton"];
+ public string CancelButtonText => _localizer["CancelButton"];
+
+ partial void OnSelectedMergeTargetCompilationChanged(SoftwareCompilationDto? value)
+ {
+ MergeSelectedSources.Clear();
+ MergeSourceSearchText = string.Empty;
+ MergeSourceSuggestions.Clear();
+ OnPropertyChanged(nameof(CanConfirmMerge));
+ }
+
+ partial void OnMergeSourcePickerValueChanged(SoftwareCompilationDto? value)
+ {
+ if(value?.Id is null) return;
+
+ if(MergeSelectedSources.All(s => s.Id != value.Id))
+ MergeSelectedSources.Add(value);
+
+ MergeSourcePickerValue = null;
+ MergeSourceSearchText = string.Empty;
+ MergeSourceSuggestions.Clear();
+ OnPropertyChanged(nameof(CanConfirmMerge));
+ }
+
+ private void OpenMerge()
+ {
+ CancelEdit();
+ HasError = false;
+ ErrorMessage = string.Empty;
+ IsMerging = true;
+ MergeTargetSearchText = string.Empty;
+ MergeTargetSuggestions.Clear();
+ SelectedMergeTargetCompilation = null;
+ MergeSourceSearchText = string.Empty;
+ MergeSourceSuggestions.Clear();
+ MergeSelectedSources.Clear();
+ }
+
+ public void UpdateMergeTargetSuggestions(string query) => Debounce(ref _mergeTargetDebounce, async () =>
+ {
+ List results = await _service.SearchAsync(query);
+ MergeTargetSuggestions.Clear();
+ foreach(SoftwareCompilationDto c in results) MergeTargetSuggestions.Add(c);
+ });
+
+ public void UpdateMergeSourceSuggestions(string query) => Debounce(ref _mergeSourceDebounce, async () =>
+ {
+ List results = await _service.SearchAsync(query);
+
+ MergeSourceSuggestions.Clear();
+
+ foreach(SoftwareCompilationDto c in results)
+ if(c.Id != SelectedMergeTargetCompilation?.Id && MergeSelectedSources.All(s => s.Id != c.Id))
+ MergeSourceSuggestions.Add(c);
+ });
+
+ private void RemoveMergeSource(SoftwareCompilationDto? item)
+ {
+ if(item == null) return;
+
+ MergeSelectedSources.Remove(item);
+ OnPropertyChanged(nameof(CanConfirmMerge));
+ }
+
+ private async Task ConfirmMergeAsync()
+ {
+ if(SelectedMergeTargetCompilation?.Id is not int targetId) return;
+
+ List sourceIds = MergeSelectedSources
+ .Where(c => c.Id is not null)
+ .Select(c => c.Id!.Value)
+ .ToList();
+
+ if(sourceIds.Count == 0) return;
+
+ try
+ {
+ HasError = false;
+ ErrorMessage = string.Empty;
+
+ bool succeeded = await _service.MergeAsync(targetId, sourceIds);
+
+ if(!succeeded)
+ {
+ ErrorMessage = _localizer["FailedToMergeSoftwareCompilations"];
+ HasError = true;
+
+ return;
+ }
+
+ CancelMerge();
+ await LoadAsync();
+ }
+ catch(Exception ex)
+ {
+ _logger.LogError(ex, "Error merging software compilations into {TargetId}", targetId);
+ ErrorMessage = _localizer["FailedToMergeSoftwareCompilations"];
+ HasError = true;
+ }
+ }
+
+ private void CancelMerge()
+ {
+ IsMerging = false;
+ MergeTargetSearchText = string.Empty;
+ MergeTargetSuggestions.Clear();
+ SelectedMergeTargetCompilation = null;
+ MergeSourceSearchText = string.Empty;
+ MergeSourceSuggestions.Clear();
+ MergeSelectedSources.Clear();
+ HasError = false;
+ ErrorMessage = string.Empty;
+ OnPropertyChanged(nameof(CanConfirmMerge));
+ }
}
diff --git a/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml b/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml
index c931778b..404c6373 100644
--- a/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml
+++ b/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml
@@ -65,6 +65,8 @@
+
@@ -301,6 +303,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml.cs b/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml.cs
index 7a2739f8..4f747937 100644
--- a/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml.cs
+++ b/Marechai.App/Presentation/Views/Admin/AdminSoftwareCompilationsPage.xaml.cs
@@ -1,3 +1,4 @@
+using System;
using Marechai.ApiClient.Models;
using Marechai.App.Presentation.ViewModels.Admin;
using Microsoft.UI.Xaml;
@@ -59,4 +60,38 @@ public sealed partial class AdminSoftwareCompilationsPage : Page
if(DataContext is AdminSoftwareCompilationsViewModel vm && sender is ComboBox { SelectedItem: SoftwareDto software })
vm.SelectSoftwareForVersions(software);
}
+
+ private void MergeTargetFilterBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
+ {
+ if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput &&
+ DataContext is AdminSoftwareCompilationsViewModel vm)
+ vm.UpdateMergeTargetSuggestions(sender.Text);
+ }
+
+ private void MergeSourceFilterBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
+ {
+ if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput &&
+ DataContext is AdminSoftwareCompilationsViewModel vm)
+ vm.UpdateMergeSourceSuggestions(sender.Text);
+ }
+
+ private async void MergeButton_Click(object sender, RoutedEventArgs e)
+ {
+ if(DataContext is not AdminSoftwareCompilationsViewModel vm) return;
+
+ var dialog = new ContentDialog
+ {
+ XamlRoot = XamlRoot,
+ Title = vm.MergeConfirmDialogTitle,
+ PrimaryButtonText = vm.MergeButtonText,
+ CloseButtonText = vm.CancelButtonText,
+ DefaultButton = ContentDialogButton.Close,
+ Content = vm.MergeConfirmDialogMessage
+ };
+
+ ContentDialogResult result = await dialog.ShowAsync();
+ if(result != ContentDialogResult.Primary) return;
+
+ await vm.ConfirmMergeCommand.ExecuteAsync(null);
+ }
}
diff --git a/Marechai.App/Resources/Resources.de.resx b/Marechai.App/Resources/Resources.de.resx
index cc8bd88b..79d25150 100644
--- a/Marechai.App/Resources/Resources.de.resx
+++ b/Marechai.App/Resources/Resources.de.resx
@@ -953,6 +953,24 @@
Softwareplattformen konnten nicht zusammengeführt werden
+
+ Kompilationen zusammenführen
+
+
+ Software-Kompilationen zusammenführen
+
+
+ Zielkompilation
+
+
+ Zu fusionierende Kompilationen
+
+
+ Wählen Sie eine oder mehrere Quellkompilationen aus, die in das Ziel zusammengeführt werden sollen. Die ausgewählten Quellkompilationen werden gelöscht, nachdem ihre Referenzen verschoben wurden.
+
+
+ Softwarekompilationen konnten nicht zusammengeführt werden
+
Unternehmen konnten nicht zusammengeführt werden
diff --git a/Marechai.App/Resources/Resources.es.resx b/Marechai.App/Resources/Resources.es.resx
index e1288749..25c26667 100644
--- a/Marechai.App/Resources/Resources.es.resx
+++ b/Marechai.App/Resources/Resources.es.resx
@@ -953,6 +953,24 @@
No se pudieron fusionar las plataformas de software
+
+ Fusionar compilaciones
+
+
+ Fusionar compilaciones de software
+
+
+ Compilación de destino
+
+
+ Compilaciones a fusionar
+
+
+ Seleccione una o más compilaciones de origen para fusionar en el destino. Las compilaciones de origen seleccionadas se eliminarán después de mover sus referencias.
+
+
+ No se pudieron fusionar las compilaciones de software
+
No se pudieron fusionar las empresas
diff --git a/Marechai.App/Resources/Resources.fr.resx b/Marechai.App/Resources/Resources.fr.resx
index 7fac2d14..8777e960 100644
--- a/Marechai.App/Resources/Resources.fr.resx
+++ b/Marechai.App/Resources/Resources.fr.resx
@@ -953,6 +953,24 @@
Impossible de fusionner les plateformes logicielles
+
+ Fusionner les compilations
+
+
+ Fusionner les compilations logicielles
+
+
+ Compilation cible
+
+
+ Compilations à fusionner
+
+
+ Sélectionnez une ou plusieurs compilations source à fusionner dans la cible. Les compilations source sélectionnées seront supprimées après le déplacement de leurs références.
+
+
+ Impossible de fusionner les compilations logicielles
+
Impossible de fusionner les entreprises
diff --git a/Marechai.App/Resources/Resources.pt-BR.resx b/Marechai.App/Resources/Resources.pt-BR.resx
index 62ccac70..65e15bee 100644
--- a/Marechai.App/Resources/Resources.pt-BR.resx
+++ b/Marechai.App/Resources/Resources.pt-BR.resx
@@ -953,6 +953,24 @@
Falha ao mesclar plataformas de software
+
+ Mesclar compilações
+
+
+ Mesclar compilações de software
+
+
+ Compilação de destino
+
+
+ Compilações para mesclar
+
+
+ Selecione uma ou mais compilações de origem para mesclar no destino. As compilações de origem selecionadas serão excluídas após a movimentação de suas referências.
+
+
+ Falha ao mesclar compilações de software
+
Falha ao mesclar empresas
diff --git a/Marechai.App/Resources/Resources.resx b/Marechai.App/Resources/Resources.resx
index 26911e8c..6c8e8e6e 100644
--- a/Marechai.App/Resources/Resources.resx
+++ b/Marechai.App/Resources/Resources.resx
@@ -1052,6 +1052,24 @@
Failed to merge software platforms
+
+ Merge Compilations
+
+
+ Merge Software Compilations
+
+
+ Target compilation
+
+
+ Compilations to merge
+
+
+ Select one or more source compilations to merge into the target. The selected source compilations will be deleted after their references are moved.
+
+
+ Failed to merge software compilations
+
Failed to merge companies
diff --git a/Marechai.App/Services/SoftwareCompilationsService.cs b/Marechai.App/Services/SoftwareCompilationsService.cs
index bfc6d884..32878c9d 100644
--- a/Marechai.App/Services/SoftwareCompilationsService.cs
+++ b/Marechai.App/Services/SoftwareCompilationsService.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
namespace Marechai.App.Services;
@@ -175,6 +176,28 @@ public sealed class SoftwareCompilationsService
}
}
+ public async Task MergeAsync(int targetId, IReadOnlyList sourceIds)
+ {
+ try
+ {
+ var request = new MergeSoftwareCompilationsRequest
+ {
+ TargetId = targetId,
+ SourceIds = sourceIds.Select(id => (int?)id).ToList()
+ };
+
+ await _apiClient.SoftwareCompilations[targetId].Merge.PostAsync(request);
+
+ return true;
+ }
+ catch(Exception ex)
+ {
+ _logger.LogError(ex, "Error merging software compilations {SourceIds} into {TargetId}", sourceIds, targetId);
+
+ return false;
+ }
+ }
+
public async Task AddIncludedSoftwareAsync(SoftwareBySoftwareCompilationDto dto)
{
try
diff --git a/Marechai.Data/Dtos/MergeSoftwareCompilationsRequest.cs b/Marechai.Data/Dtos/MergeSoftwareCompilationsRequest.cs
new file mode 100644
index 00000000..61f28a7d
--- /dev/null
+++ b/Marechai.Data/Dtos/MergeSoftwareCompilationsRequest.cs
@@ -0,0 +1,41 @@
+/******************************************************************************
+// MARECHAI: Master repository of computing history artifacts information
+// ----------------------------------------------------------------------------
+//
+// Author(s) : Natalia Portillo
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2003-2026 Natalia Portillo
+*******************************************************************************/
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+namespace Marechai.Data.Dtos;
+
+public class MergeSoftwareCompilationsRequest
+{
+ [JsonPropertyName("targetId")]
+ [Required]
+ public ulong TargetId { get; set; }
+
+ [JsonPropertyName("sourceIds")]
+ [Required]
+ public required List SourceIds { get; set; }
+}
diff --git a/Marechai.Server/Controllers/SoftwareCompilationsController.cs b/Marechai.Server/Controllers/SoftwareCompilationsController.cs
index 8f255a3e..c84442b3 100644
--- a/Marechai.Server/Controllers/SoftwareCompilationsController.cs
+++ b/Marechai.Server/Controllers/SoftwareCompilationsController.cs
@@ -179,6 +179,181 @@ public class SoftwareCompilationsController(MarechaiContext context) : Controlle
return Ok();
}
+ [HttpPost("{id:ulong}/merge")]
+ [Authorize(Roles = "Admin,UberAdmin")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task MergeAsync(ulong id, [FromBody] MergeSoftwareCompilationsRequest request)
+ {
+ string userId = User.FindFirstValue(ClaimTypes.Sid);
+
+ if(userId is null) return Unauthorized();
+
+ if(request?.SourceIds == null || request.SourceIds.Count == 0)
+ return Problem(detail: "At least one source compilation must be specified", statusCode: StatusCodes.Status400BadRequest);
+
+ if(request.SourceIds.Contains(id))
+ return Problem(detail: "Target compilation cannot be in the source list", statusCode: StatusCodes.Status400BadRequest);
+
+ SoftwareCompilation target = await context.SoftwareCompilations.FindAsync(id);
+
+ if(target is null) return NotFound("Target compilation not found");
+
+ var sourcesToDelete = await context.SoftwareCompilations
+ .Where(c => request.SourceIds.Contains(c.Id))
+ .ToListAsync();
+
+ if(sourcesToDelete.Count != request.SourceIds.Count)
+ return NotFound("One or more source compilations not found");
+
+ await using var transaction = await context.Database.BeginTransactionAsync();
+
+ try
+ {
+ await context.SoftwareReleases
+ .Where(r => r.SoftwareCompilationId.HasValue &&
+ request.SourceIds.Contains(r.SoftwareCompilationId.Value))
+ .ExecuteUpdateAsync(setters =>
+ setters.SetProperty(r => r.SoftwareCompilationId, (ulong?)id));
+
+ await context.SoftwareCovers
+ .Where(c => c.SoftwareCompilationId.HasValue &&
+ request.SourceIds.Contains(c.SoftwareCompilationId.Value))
+ .ExecuteUpdateAsync(setters =>
+ setters.SetProperty(c => c.SoftwareCompilationId, (ulong?)id));
+
+ // --- SoftwareBySoftwareCompilation: dedupe on (target id, SoftwareId) ---
+
+ var targetSoftwareKeys = (await context.SoftwareBySoftwareCompilation
+ .Where(x => x.SoftwareCompilationId == id)
+ .Select(x => x.SoftwareId)
+ .ToListAsync())
+ .ToHashSet();
+
+ var sourceSoftwareLinks = await context.SoftwareBySoftwareCompilation
+ .Where(x => request.SourceIds.Contains(x.SoftwareCompilationId))
+ .ToListAsync();
+
+ foreach(SoftwareBySoftwareCompilation link in sourceSoftwareLinks)
+ {
+ context.SoftwareBySoftwareCompilation.Remove(link);
+
+ if(targetSoftwareKeys.Add(link.SoftwareId))
+ await context.SoftwareBySoftwareCompilation.AddAsync(new SoftwareBySoftwareCompilation
+ {
+ SoftwareCompilationId = id,
+ SoftwareId = link.SoftwareId
+ });
+ }
+
+ // --- SoftwareVersionBySoftwareCompilation: dedupe on (target id, SoftwareVersionId) ---
+
+ var targetVersionKeys = (await context.SoftwareVersionBySoftwareCompilation
+ .Where(x => x.SoftwareCompilationId == id)
+ .Select(x => x.SoftwareVersionId)
+ .ToListAsync())
+ .ToHashSet();
+
+ var sourceVersionLinks = await context.SoftwareVersionBySoftwareCompilation
+ .Where(x => request.SourceIds.Contains(x.SoftwareCompilationId))
+ .ToListAsync();
+
+ foreach(SoftwareVersionBySoftwareCompilation link in sourceVersionLinks)
+ {
+ context.SoftwareVersionBySoftwareCompilation.Remove(link);
+
+ if(targetVersionKeys.Add(link.SoftwareVersionId))
+ await context.SoftwareVersionBySoftwareCompilation.AddAsync(new SoftwareVersionBySoftwareCompilation
+ {
+ SoftwareCompilationId = id,
+ SoftwareVersionId = link.SoftwareVersionId
+ });
+ }
+
+ // --- SoftwareCompilationBySoftwareCompilation: re-parent both roles, dedupe and drop self-loops ---
+
+ var targetParentKeys = (await context.SoftwareCompilationBySoftwareCompilation
+ .Where(x => x.ParentCompilationId == id)
+ .Select(x => x.ChildCompilationId)
+ .ToListAsync())
+ .ToHashSet();
+
+ var sourceAsParentLinks = await context.SoftwareCompilationBySoftwareCompilation
+ .Where(x => request.SourceIds.Contains(x.ParentCompilationId))
+ .ToListAsync();
+
+ foreach(SoftwareCompilationBySoftwareCompilation link in sourceAsParentLinks)
+ {
+ context.SoftwareCompilationBySoftwareCompilation.Remove(link);
+
+ if(link.ChildCompilationId == id) continue; // would become a self-loop
+
+ if(targetParentKeys.Add(link.ChildCompilationId))
+ await context.SoftwareCompilationBySoftwareCompilation.AddAsync(
+ new SoftwareCompilationBySoftwareCompilation
+ {
+ ParentCompilationId = id,
+ ChildCompilationId = link.ChildCompilationId
+ });
+ }
+
+ var targetChildKeys = (await context.SoftwareCompilationBySoftwareCompilation
+ .Where(x => x.ChildCompilationId == id)
+ .Select(x => x.ParentCompilationId)
+ .ToListAsync())
+ .ToHashSet();
+
+ var sourceAsChildLinks = await context.SoftwareCompilationBySoftwareCompilation
+ .Where(x => request.SourceIds.Contains(x.ChildCompilationId))
+ .ToListAsync();
+
+ foreach(SoftwareCompilationBySoftwareCompilation link in sourceAsChildLinks)
+ {
+ context.SoftwareCompilationBySoftwareCompilation.Remove(link);
+
+ if(link.ParentCompilationId == id) continue; // would become a self-loop
+
+ if(targetChildKeys.Add(link.ParentCompilationId))
+ await context.SoftwareCompilationBySoftwareCompilation.AddAsync(
+ new SoftwareCompilationBySoftwareCompilation
+ {
+ ParentCompilationId = link.ParentCompilationId,
+ ChildCompilationId = id
+ });
+ }
+
+ // --- Predecessor self-reference ---
+
+ await context.SoftwareCompilations
+ .Where(c => c.PredecessorId.HasValue &&
+ request.SourceIds.Contains(c.PredecessorId.Value) &&
+ !request.SourceIds.Contains(c.Id))
+ .ExecuteUpdateAsync(setters =>
+ setters.SetProperty(c => c.PredecessorId, (ulong?)id));
+
+ if(target.PredecessorId.HasValue && request.SourceIds.Contains(target.PredecessorId.Value))
+ target.PredecessorId = null;
+
+ foreach(SoftwareCompilation source in sourcesToDelete)
+ context.SoftwareCompilations.Remove(source);
+
+ await context.SaveChangesWithUserAsync(userId);
+
+ await transaction.CommitAsync();
+ }
+ catch(Exception ex)
+ {
+ await transaction.RollbackAsync();
+
+ return Problem(detail: $"Merge failed: {ex.Message}", statusCode: StatusCodes.Status409Conflict);
+ }
+
+ return Ok();
+ }
+
[HttpGet("{id:ulong}/releases")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
diff --git a/Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor b/Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor
new file mode 100644
index 00000000..6f58a2c5
--- /dev/null
+++ b/Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor
@@ -0,0 +1,73 @@
+@using System.Threading
+@using Marechai.ApiClient.Models
+@using Marechai.Services
+@inject SoftwareCompilationsService SoftwareCompilationsService
+@inject IStringLocalizer L
+
+
+
+
+
+ @L["Select a target compilation and the compilations to merge into it. The target compilation will keep its name and ID, and all references will be updated."]
+
+
+
+ @L["Target Compilation"]
+
+
+
+ @if(SelectedTarget != null)
+ {
+
+ @L["Compilations to Merge"]
+
+ @L["Search and add one or more compilations to merge into the target. The selected compilations will be deleted after merging their data."]
+
+
+
+
+ @if(SelectedSources.Count > 0)
+ {
+
+ @foreach(var source in SelectedSources)
+ {
+ @source.Name
+ }
+
+
+
+ @L["Will merge"] @SelectedSources.Count @L["compilation(s) into"] "@SelectedTarget.Name".
+
+ }
+
+ }
+
+
+
+ @L["Cancel"]
+
+ @L["Merge Compilations"]
+
+
+
diff --git a/Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor.cs b/Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor.cs
new file mode 100644
index 00000000..dcbf9203
--- /dev/null
+++ b/Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Marechai.ApiClient.Models;
+using Microsoft.AspNetCore.Components;
+using MudBlazor;
+
+namespace Marechai.Pages.Admin;
+
+public partial class MergeSoftwareCompilationsDialog
+{
+ [CascadingParameter]
+ IMudDialogInstance MudDialog { get; set; } = null!;
+
+ SoftwareCompilationDto SelectedTarget { get; set; }
+ List SelectedSources { get; set; } = [];
+ SoftwareCompilationDto _sourcePickerValue;
+
+ async Task> SearchTargetAsync(string value, CancellationToken ct)
+ {
+ if(string.IsNullOrWhiteSpace(value)) return [];
+
+ return await SoftwareCompilationsService.GetPagedAsync(0, 20, value);
+ }
+
+ async Task> SearchSourcesAsync(string value, CancellationToken ct)
+ {
+ if(string.IsNullOrWhiteSpace(value)) return [];
+
+ List results = await SoftwareCompilationsService.GetPagedAsync(0, 20, value);
+
+ return results.Where(c => c.Id != SelectedTarget?.Id &&
+ SelectedSources.All(s => s.Id != c.Id));
+ }
+
+ void OnTargetChanged(SoftwareCompilationDto compilation)
+ {
+ SelectedTarget = compilation;
+ SelectedSources.Clear();
+ _sourcePickerValue = null;
+ }
+
+ void OnSourcePicked(SoftwareCompilationDto compilation)
+ {
+ if(compilation is null) return;
+
+ if(SelectedSources.All(s => s.Id != compilation.Id)) SelectedSources.Add(compilation);
+
+ _sourcePickerValue = null;
+ }
+
+ void RemoveSource(SoftwareCompilationDto compilation) => SelectedSources.Remove(compilation);
+
+ void Cancel() => MudDialog.Cancel();
+
+ void Merge()
+ {
+ var result = new MergeSoftwareCompilationsDialogResult
+ {
+ TargetId = (ulong)(SelectedTarget.Id ?? 0),
+ SourceIds = SelectedSources.Select(s => (ulong)(s.Id ?? 0)).ToList()
+ };
+
+ MudDialog.Close(DialogResult.Ok(result));
+ }
+}
diff --git a/Marechai/Pages/Admin/MergeSoftwareCompilationsDialogResult.cs b/Marechai/Pages/Admin/MergeSoftwareCompilationsDialogResult.cs
new file mode 100644
index 00000000..57e86669
--- /dev/null
+++ b/Marechai/Pages/Admin/MergeSoftwareCompilationsDialogResult.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Marechai.Pages.Admin;
+
+public class MergeSoftwareCompilationsDialogResult
+{
+ public ulong TargetId { get; set; }
+ public List SourceIds { get; set; }
+}
diff --git a/Marechai/Pages/Admin/SoftwareCompilations.razor b/Marechai/Pages/Admin/SoftwareCompilations.razor
index 02493240..ea920114 100644
--- a/Marechai/Pages/Admin/SoftwareCompilations.razor
+++ b/Marechai/Pages/Admin/SoftwareCompilations.razor
@@ -28,6 +28,7 @@
@L["Add Compilation"]
+ @L["Merge Compilations"]
(L["Merge Compilations"], new DialogOptions
+ {
+ MaxWidth = MaxWidth.Small,
+ FullWidth = true
+ });
+
+ DialogResult result = await dialog.Result;
+
+ if(result is { Canceled: false, Data: MergeSoftwareCompilationsDialogResult data })
+ {
+ (bool succeeded, string errorMessage) =
+ await SoftwareCompilationsService.MergeAsync(data.TargetId, data.SourceIds);
+
+ if(succeeded)
+ {
+ _successMessage = L["Compilations merged successfully."];
+ await _dataGrid.ReloadServerData();
+ }
+ else
+ {
+ _errorMessage = errorMessage;
+ }
+ }
+ }
+
async Task ConfirmDelete(SoftwareCompilationDto compilation)
{
DialogParameters parameters = new()
diff --git a/Marechai/Resources/Services/SoftwareCompilationsService.de.resx b/Marechai/Resources/Services/SoftwareCompilationsService.de.resx
new file mode 100644
index 00000000..3e796b03
--- /dev/null
+++ b/Marechai/Resources/Services/SoftwareCompilationsService.de.resx
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ Kompilationen zusammenführen
+
+
+ Kompilationen erfolgreich zusammengeführt.
+
+
+ Zielkompilation
+
+
+ Zielkompilation auswählen
+
+
+ Zu fusionierende Kompilationen
+
+
+ Suchen und fügen Sie eine oder mehrere Kompilationen hinzu, die in das Ziel zusammengeführt werden sollen. Die ausgewählten Kompilationen werden nach dem Zusammenführen ihrer Daten gelöscht.
+
+
+ Wird zusammenführen
+
+
+ Kompilation(en) in
+
+
+ Wählen Sie eine Zielkompilation und die Kompilationen aus, die damit zusammengeführt werden sollen. Die Zielkompilation behält ihren Namen und ihre ID, und alle Referenzen werden aktualisiert.
+
+
+ Kompilation zum Hinzufügen suchen
+
+
diff --git a/Marechai/Resources/Services/SoftwareCompilationsService.es.resx b/Marechai/Resources/Services/SoftwareCompilationsService.es.resx
new file mode 100644
index 00000000..79e41503
--- /dev/null
+++ b/Marechai/Resources/Services/SoftwareCompilationsService.es.resx
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ Fusionar compilaciones
+
+
+ Compilaciones fusionadas correctamente.
+
+
+ Compilación de destino
+
+
+ Seleccionar compilación de destino
+
+
+ Compilaciones a fusionar
+
+
+ Busque y añada una o más compilaciones para fusionar en el destino. Las compilaciones seleccionadas se eliminarán después de fusionar sus datos.
+
+
+ Se fusionarán
+
+
+ compilación(es) en
+
+
+ Seleccione una compilación de destino y las compilaciones a fusionar en ella. La compilación de destino mantendrá su nombre e ID, y todas las referencias se actualizarán.
+
+
+ Buscar compilación para añadir
+
+
diff --git a/Marechai/Resources/Services/SoftwareCompilationsService.fr.resx b/Marechai/Resources/Services/SoftwareCompilationsService.fr.resx
new file mode 100644
index 00000000..497fcc71
--- /dev/null
+++ b/Marechai/Resources/Services/SoftwareCompilationsService.fr.resx
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ Fusionner les compilations
+
+
+ Compilations fusionnées avec succès.
+
+
+ Compilation cible
+
+
+ Sélectionner la compilation cible
+
+
+ Compilations à fusionner
+
+
+ Recherchez et ajoutez une ou plusieurs compilations à fusionner dans la cible. Les compilations sélectionnées seront supprimées après la fusion de leurs données.
+
+
+ Fusionnera
+
+
+ compilation(s) dans
+
+
+ Sélectionnez une compilation cible et les compilations à y fusionner. La compilation cible conservera son nom et son ID, et toutes les références seront mises à jour.
+
+
+ Rechercher une compilation à ajouter
+
+
diff --git a/Marechai/Resources/Services/SoftwareCompilationsService.it.resx b/Marechai/Resources/Services/SoftwareCompilationsService.it.resx
new file mode 100644
index 00000000..eaa2cd30
--- /dev/null
+++ b/Marechai/Resources/Services/SoftwareCompilationsService.it.resx
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+
+
+
+ Unisci compilazioni
+
+
+ Compilazioni unite con successo.
+
+
+ Compilazione di destinazione
+
+
+ Seleziona compilazione di destinazione
+
+
+ Compilazioni da unire
+
+
+ Cerca e aggiungi una o più compilazioni da unire alla destinazione. Le compilazioni selezionate verranno eliminate dopo l'unione dei loro dati.
+
+
+ Verranno unite
+
+
+ compilazione/i in
+
+
+ Selezionare una compilazione di destinazione e le compilazioni da unire ad essa. La compilazione di destinazione manterrà il proprio nome e ID, e tutti i riferimenti verranno aggiornati.
+
+
+ Cerca compilazione da aggiungere
+
+
diff --git a/Marechai/Services/SoftwareCompilationsService.cs b/Marechai/Services/SoftwareCompilationsService.cs
index 9ec1ebd5..1aafb9b5 100644
--- a/Marechai/Services/SoftwareCompilationsService.cs
+++ b/Marechai/Services/SoftwareCompilationsService.cs
@@ -137,6 +137,30 @@ public class SoftwareCompilationsService(Marechai.ApiClient.Client client)
}
}
+ public async Task<(bool succeeded, string error)> MergeAsync(ulong targetId, List sourceIds)
+ {
+ try
+ {
+ var request = new MergeSoftwareCompilationsRequest
+ {
+ TargetId = (int?)targetId,
+ SourceIds = sourceIds.ConvertAll(sourceId => (int?)sourceId)
+ };
+
+ await client.SoftwareCompilations[(int)targetId].Merge.PostAsync(request);
+
+ return (true, null);
+ }
+ catch(ApiException ex)
+ {
+ return (false, ExtractDetail(ex));
+ }
+ catch(Exception ex)
+ {
+ return (false, ex.Message);
+ }
+ }
+
public async Task> GetReleasesAsync(int id)
{
try