mirror of
https://github.com/claunia/marechai.git
synced 2026-07-08 17:57:08 +00:00
Add software compilation merge to Blazor and Uno admin apps
Lets admins merge duplicate SoftwareCompilation entries: a new
POST /software-compilations/{id}/merge endpoint re-parents releases,
covers, junction rows (with dedup/self-loop guards), and predecessor
references onto the target before deleting the sources. Wired into
both the Blazor MudBlazor admin page and the Uno admin page, each
with a search-based target/source picker, plus full localization.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// <auto-generated/>
|
||||
#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
|
||||
{
|
||||
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
|
||||
public IDictionary<string, object> AdditionalData { get; set; }
|
||||
/// <summary>The sourceIds property</summary>
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
public List<int?>? SourceIds { get; set; }
|
||||
#nullable restore
|
||||
#else
|
||||
public List<int?> SourceIds { get; set; }
|
||||
#endif
|
||||
/// <summary>The targetId property</summary>
|
||||
public int? TargetId { get; set; }
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest"/> and sets the default values.
|
||||
/// </summary>
|
||||
public MergeSoftwareCompilationsRequest()
|
||||
{
|
||||
AdditionalData = new Dictionary<string, object>();
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates a new instance of the appropriate class based on discriminator value
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest"/></returns>
|
||||
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
|
||||
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();
|
||||
}
|
||||
/// <summary>
|
||||
/// The deserialization information for the current model
|
||||
/// </summary>
|
||||
/// <returns>A IDictionary<string, Action<IParseNode>></returns>
|
||||
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
|
||||
{
|
||||
return new Dictionary<string, Action<IParseNode>>
|
||||
{
|
||||
{ "sourceIds", n => { SourceIds = n.GetCollectionOfPrimitiveValues<int?>()?.AsList(); } },
|
||||
{ "targetId", n => { TargetId = n.GetIntValue(); } },
|
||||
};
|
||||
}
|
||||
/// <summary>
|
||||
/// Serializes information the current object
|
||||
/// </summary>
|
||||
/// <param name="writer">Serialization writer to use to serialize this model</param>
|
||||
public virtual void Serialize(ISerializationWriter writer)
|
||||
{
|
||||
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
|
||||
writer.WriteCollectionOfPrimitiveValues<int?>("sourceIds", SourceIds);
|
||||
writer.WriteIntValue("targetId", TargetId);
|
||||
writer.WriteAdditionalData(AdditionalData);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
@@ -0,0 +1,102 @@
|
||||
// <auto-generated/>
|
||||
#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
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds and executes requests for operations under \software-compilations\{id}\merge
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
|
||||
public partial class MergeRequestBuilder : BaseRequestBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder"/> and sets the default values.
|
||||
/// </summary>
|
||||
/// <param name="pathParameters">Path parameters for the request</param>
|
||||
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
|
||||
public MergeRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software-compilations/{id}/merge", pathParameters)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder"/> and sets the default values.
|
||||
/// </summary>
|
||||
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
|
||||
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
|
||||
public MergeRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software-compilations/{id}/merge", rawUrl)
|
||||
{
|
||||
}
|
||||
/// <returns>A <see cref="Stream"/></returns>
|
||||
/// <param name="body">The request body</param>
|
||||
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
|
||||
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
||||
/// <exception cref="global::Marechai.ApiClient.Models.ProblemDetails">When receiving a 400 status code</exception>
|
||||
/// <exception cref="global::Marechai.ApiClient.Models.ProblemDetails">When receiving a 401 status code</exception>
|
||||
/// <exception cref="global::Marechai.ApiClient.Models.ProblemDetails">When receiving a 404 status code</exception>
|
||||
/// <exception cref="global::Marechai.ApiClient.Models.ProblemDetails">When receiving a 409 status code</exception>
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
public async Task<Stream?> PostAsync(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#nullable restore
|
||||
#else
|
||||
public async Task<Stream> PostAsync(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#endif
|
||||
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
|
||||
var requestInfo = ToPostRequestInformation(body, requestConfiguration);
|
||||
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
|
||||
{
|
||||
{ "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<Stream>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
/// <returns>A <see cref="RequestInformation"/></returns>
|
||||
/// <param name="body">The request body</param>
|
||||
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
|
||||
{
|
||||
#nullable restore
|
||||
#else
|
||||
public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.MergeSoftwareCompilationsRequest body, Action<RequestConfiguration<DefaultQueryParameters>> 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;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder"/></returns>
|
||||
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
|
||||
public global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder WithUrl(string rawUrl)
|
||||
{
|
||||
return new global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder(rawUrl, RequestAdapter);
|
||||
}
|
||||
/// <summary>
|
||||
/// Configuration for the request such as headers, query parameters, and middleware options.
|
||||
/// </summary>
|
||||
[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<DefaultQueryParameters>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
@@ -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);
|
||||
}
|
||||
/// <summary>The merge property</summary>
|
||||
public global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder Merge
|
||||
{
|
||||
get => new global::Marechai.ApiClient.SoftwareCompilations.Item.Merge.MergeRequestBuilder(PathParameters, RequestAdapter);
|
||||
}
|
||||
/// <summary>The releases property</summary>
|
||||
public global::Marechai.ApiClient.SoftwareCompilations.Item.Releases.ReleasesRequestBuilder Releases
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"descriptionHash": "EF23CFA3E3D7BEEBEDACF0F1C162F3E65C5100A4E247529FC19A317185AC443E038F5D1F706BED6C0A696F2ABF5E1C4CECB13352C16548B9812C094FC0E068CC",
|
||||
"descriptionHash": "0AA7E1D641D98EEE0410A0EB52E5A5C64562D47536F6BA516F5AF89FD4902F6253992E5ABA435A45DEB050C4C933C2E34B1474F7D82962B9971E337948E133B7",
|
||||
"descriptionLocation": "../../../../../tmp/openapi.json",
|
||||
"lockFileVersion": "1.0.0",
|
||||
"kiotaVersion": "1.31.1",
|
||||
|
||||
@@ -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<MachineDto>? _allMachines;
|
||||
@@ -91,6 +93,18 @@ public partial class AdminSoftwareCompilationsViewModel : ObservableObject, IReg
|
||||
[ObservableProperty] private ObservableCollection<SoftwareCompilationDto> _includedCompilationSuggestions = [];
|
||||
[ObservableProperty] private SoftwareCompilationDto? _selectedIncludedCompilation;
|
||||
|
||||
// --- Merge ---
|
||||
[ObservableProperty] private bool _isMerging;
|
||||
[ObservableProperty] private string _mergeTargetSearchText = string.Empty;
|
||||
[ObservableProperty] private ObservableCollection<SoftwareCompilationDto> _mergeTargetSuggestions = [];
|
||||
[ObservableProperty] private SoftwareCompilationDto? _selectedMergeTargetCompilation;
|
||||
[ObservableProperty] private string _mergeSourceSearchText = string.Empty;
|
||||
[ObservableProperty] private ObservableCollection<SoftwareCompilationDto> _mergeSourceSuggestions = [];
|
||||
[ObservableProperty] private SoftwareCompilationDto? _mergeSourcePickerValue;
|
||||
[ObservableProperty] private ObservableCollection<SoftwareCompilationDto> _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<SoftwareCompilationDto>(RemoveIncludedCompilationAsync);
|
||||
|
||||
OpenMergeCommand = new RelayCommand(OpenMerge);
|
||||
ConfirmMergeCommand = new AsyncRelayCommand(ConfirmMergeAsync);
|
||||
CancelMergeCommand = new RelayCommand(CancelMerge);
|
||||
RemoveMergeSourceCommand = new RelayCommand<SoftwareCompilationDto>(RemoveMergeSource);
|
||||
|
||||
CheckAdminRole();
|
||||
}
|
||||
|
||||
@@ -147,6 +166,11 @@ public partial class AdminSoftwareCompilationsViewModel : ObservableObject, IReg
|
||||
public IAsyncRelayCommand AddIncludedCompilationCommand { get; }
|
||||
public IAsyncRelayCommand<SoftwareCompilationDto> RemoveIncludedCompilationCommand { get; }
|
||||
|
||||
public IRelayCommand OpenMergeCommand { get; }
|
||||
public IAsyncRelayCommand ConfirmMergeCommand { get; }
|
||||
public IRelayCommand CancelMergeCommand { get; }
|
||||
public IRelayCommand<SoftwareCompilationDto> 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<SoftwareCompilationDto> 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<SoftwareCompilationDto> 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<int> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@
|
||||
<CommandBar Grid.Row="0" DefaultLabelPosition="Right">
|
||||
<AppBarButton Icon="Add" Label="{Binding Source={StaticResource Strings}, Path=AddButton}"
|
||||
Command="{Binding OpenAddCommand}" />
|
||||
<AppBarButton Icon="Switch" Label="{Binding Source={StaticResource Strings}, Path=MergeCompilationsButton}"
|
||||
Command="{Binding OpenMergeCommand}" />
|
||||
<AppBarButton Icon="Refresh" Label="{Binding Source={StaticResource Strings}, Path=RefreshButton}"
|
||||
Command="{Binding LoadCommand}" />
|
||||
</CommandBar>
|
||||
@@ -301,6 +303,100 @@
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Merge panel -->
|
||||
<Border Grid.Column="1" Visibility="{Binding IsMerging, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Width="420" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" BorderThickness="1,0,0,0">
|
||||
<Grid RowSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Padding="16,12" BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" BorderThickness="0,0,0,1">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=MergeCompilationsPanelTitle}"
|
||||
FontSize="18" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding SelectedMergeTargetCompilation.Name}" FontSize="13"
|
||||
Foreground="{ThemeResource SystemBaseMediumColor}" Margin="0,2,0,0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1" Padding="16" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="12">
|
||||
<AutoSuggestBox Header="{Binding Source={StaticResource Strings}, Path=MergeCompilationsTargetLabel}"
|
||||
PlaceholderText="{Binding Source={StaticResource Strings}, Path=SearchPlaceholder}"
|
||||
Text="{Binding MergeTargetSearchText, Mode=TwoWay}" QueryIcon="Find"
|
||||
x:Name="MergeTargetFilterBox" TextChanged="MergeTargetFilterBox_TextChanged" />
|
||||
<ComboBox ItemsSource="{Binding MergeTargetSuggestions}"
|
||||
SelectedItem="{Binding SelectedMergeTargetCompilation, Mode=TwoWay}"
|
||||
DisplayMemberPath="Name" HorizontalAlignment="Stretch"
|
||||
PlaceholderText="{Binding Source={StaticResource Strings}, Path=MergeCompilationsTargetLabel}" />
|
||||
|
||||
<InfoBar IsOpen="True"
|
||||
Visibility="{Binding SelectedMergeTargetCompilation, Converter={StaticResource NullToVisibilityConverter}}"
|
||||
Severity="Warning"
|
||||
Title="{Binding Source={StaticResource Strings}, Path=MergeWarningTitle}"
|
||||
Message="{Binding Source={StaticResource Strings}, Path=MergeCompilationsHelpText}"
|
||||
IsClosable="False" />
|
||||
|
||||
<StackPanel Visibility="{Binding SelectedMergeTargetCompilation, Converter={StaticResource NullToVisibilityConverter}}"
|
||||
Spacing="8">
|
||||
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=MergeCompilationsSourcesLabel}"
|
||||
FontSize="16" FontWeight="SemiBold" />
|
||||
<Grid ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<AutoSuggestBox Grid.Column="0"
|
||||
PlaceholderText="{Binding Source={StaticResource Strings}, Path=SearchPlaceholder}"
|
||||
Text="{Binding MergeSourceSearchText, Mode=TwoWay}" QueryIcon="Find"
|
||||
x:Name="MergeSourceFilterBox" TextChanged="MergeSourceFilterBox_TextChanged" />
|
||||
</Grid>
|
||||
<ComboBox ItemsSource="{Binding MergeSourceSuggestions}"
|
||||
SelectedItem="{Binding MergeSourcePickerValue, Mode=TwoWay}"
|
||||
DisplayMemberPath="Name" HorizontalAlignment="Stretch"
|
||||
PlaceholderText="{Binding Source={StaticResource Strings}, Path=MergeCompilationsSourcesLabel}" />
|
||||
<ItemsRepeater ItemsSource="{Binding MergeSelectedSources}">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Spacing="4" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,2" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="{Binding Name}" />
|
||||
<Button Grid.Column="1" Content="{Binding Source={StaticResource Strings}, Path=DeleteButton}"
|
||||
Command="{Binding DataContext.RemoveMergeSourceCommand, ElementName=PageRoot}"
|
||||
CommandParameter="{Binding}" Foreground="{ThemeResource SystemErrorTextColor}"
|
||||
Style="{ThemeResource TextBlockButtonStyle}" Padding="8,4" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="2" Padding="16,12" BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0" Content="{Binding Source={StaticResource Strings}, Path=CancelButton}"
|
||||
Command="{Binding CancelMergeCommand}" HorizontalAlignment="Stretch" />
|
||||
<Button Grid.Column="1" Content="{Binding Source={StaticResource Strings}, Path=MergeButton}"
|
||||
IsEnabled="{Binding CanConfirmMerge}" Click="MergeButton_Click"
|
||||
HorizontalAlignment="Stretch" Style="{ThemeResource AccentButtonStyle}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -953,6 +953,24 @@
|
||||
<data name="FailedToMergeSoftwarePlatforms" xml:space="preserve">
|
||||
<value>Softwareplattformen konnten nicht zusammengeführt werden</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsButton" xml:space="preserve">
|
||||
<value>Kompilationen zusammenführen</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsPanelTitle" xml:space="preserve">
|
||||
<value>Software-Kompilationen zusammenführen</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsTargetLabel" xml:space="preserve">
|
||||
<value>Zielkompilation</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsSourcesLabel" xml:space="preserve">
|
||||
<value>Zu fusionierende Kompilationen</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsHelpText" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="FailedToMergeSoftwareCompilations" xml:space="preserve">
|
||||
<value>Softwarekompilationen konnten nicht zusammengeführt werden</value>
|
||||
</data>
|
||||
<data name="FailedToMergeCompanies" xml:space="preserve">
|
||||
<value>Unternehmen konnten nicht zusammengeführt werden</value>
|
||||
</data>
|
||||
|
||||
@@ -953,6 +953,24 @@
|
||||
<data name="FailedToMergeSoftwarePlatforms" xml:space="preserve">
|
||||
<value>No se pudieron fusionar las plataformas de software</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsButton" xml:space="preserve">
|
||||
<value>Fusionar compilaciones</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsPanelTitle" xml:space="preserve">
|
||||
<value>Fusionar compilaciones de software</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsTargetLabel" xml:space="preserve">
|
||||
<value>Compilación de destino</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsSourcesLabel" xml:space="preserve">
|
||||
<value>Compilaciones a fusionar</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsHelpText" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="FailedToMergeSoftwareCompilations" xml:space="preserve">
|
||||
<value>No se pudieron fusionar las compilaciones de software</value>
|
||||
</data>
|
||||
<data name="FailedToMergeCompanies" xml:space="preserve">
|
||||
<value>No se pudieron fusionar las empresas</value>
|
||||
</data>
|
||||
|
||||
@@ -953,6 +953,24 @@
|
||||
<data name="FailedToMergeSoftwarePlatforms" xml:space="preserve">
|
||||
<value>Impossible de fusionner les plateformes logicielles</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsButton" xml:space="preserve">
|
||||
<value>Fusionner les compilations</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsPanelTitle" xml:space="preserve">
|
||||
<value>Fusionner les compilations logicielles</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsTargetLabel" xml:space="preserve">
|
||||
<value>Compilation cible</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsSourcesLabel" xml:space="preserve">
|
||||
<value>Compilations à fusionner</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsHelpText" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="FailedToMergeSoftwareCompilations" xml:space="preserve">
|
||||
<value>Impossible de fusionner les compilations logicielles</value>
|
||||
</data>
|
||||
<data name="FailedToMergeCompanies" xml:space="preserve">
|
||||
<value>Impossible de fusionner les entreprises</value>
|
||||
</data>
|
||||
|
||||
@@ -953,6 +953,24 @@
|
||||
<data name="FailedToMergeSoftwarePlatforms" xml:space="preserve">
|
||||
<value>Falha ao mesclar plataformas de software</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsButton" xml:space="preserve">
|
||||
<value>Mesclar compilações</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsPanelTitle" xml:space="preserve">
|
||||
<value>Mesclar compilações de software</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsTargetLabel" xml:space="preserve">
|
||||
<value>Compilação de destino</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsSourcesLabel" xml:space="preserve">
|
||||
<value>Compilações para mesclar</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsHelpText" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="FailedToMergeSoftwareCompilations" xml:space="preserve">
|
||||
<value>Falha ao mesclar compilações de software</value>
|
||||
</data>
|
||||
<data name="FailedToMergeCompanies" xml:space="preserve">
|
||||
<value>Falha ao mesclar empresas</value>
|
||||
</data>
|
||||
|
||||
@@ -1052,6 +1052,24 @@
|
||||
<data name="FailedToMergeSoftwarePlatforms" xml:space="preserve">
|
||||
<value>Failed to merge software platforms</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsButton" xml:space="preserve">
|
||||
<value>Merge Compilations</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsPanelTitle" xml:space="preserve">
|
||||
<value>Merge Software Compilations</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsTargetLabel" xml:space="preserve">
|
||||
<value>Target compilation</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsSourcesLabel" xml:space="preserve">
|
||||
<value>Compilations to merge</value>
|
||||
</data>
|
||||
<data name="MergeCompilationsHelpText" xml:space="preserve">
|
||||
<value>Select one or more source compilations to merge into the target. The selected source compilations will be deleted after their references are moved.</value>
|
||||
</data>
|
||||
<data name="FailedToMergeSoftwareCompilations" xml:space="preserve">
|
||||
<value>Failed to merge software compilations</value>
|
||||
</data>
|
||||
<data name="FailedToMergeCompanies" xml:space="preserve">
|
||||
<value>Failed to merge companies</value>
|
||||
</data>
|
||||
|
||||
@@ -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<bool> MergeAsync(int targetId, IReadOnlyList<int> 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<bool> AddIncludedSoftwareAsync(SoftwareBySoftwareCompilationDto dto)
|
||||
{
|
||||
try
|
||||
|
||||
41
Marechai.Data/Dtos/MergeSoftwareCompilationsRequest.cs
Normal file
41
Marechai.Data/Dtos/MergeSoftwareCompilationsRequest.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
/******************************************************************************
|
||||
// MARECHAI: Master repository of computing history artifacts information
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||||
//
|
||||
// --[ 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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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<ulong> SourceIds { get; set; }
|
||||
}
|
||||
@@ -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<ActionResult> 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)]
|
||||
|
||||
73
Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor
Normal file
73
Marechai/Pages/Admin/MergeSoftwareCompilationsDialog.razor
Normal file
@@ -0,0 +1,73 @@
|
||||
@using System.Threading
|
||||
@using Marechai.ApiClient.Models
|
||||
@using Marechai.Services
|
||||
@inject SoftwareCompilationsService SoftwareCompilationsService
|
||||
@inject IStringLocalizer<SoftwareCompilationsService> L
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
@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."]
|
||||
</MudAlert>
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.subtitle2" Class="font-weight-bold">@L["Target Compilation"]</MudText>
|
||||
<MudAutocomplete T="SoftwareCompilationDto"
|
||||
Value="SelectedTarget"
|
||||
ValueChanged="OnTargetChanged"
|
||||
SearchFunc="SearchTargetAsync"
|
||||
ToStringFunc="@(c => c?.Name ?? string.Empty)"
|
||||
Label="@L["Select target compilation"]"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
DebounceInterval="300"
|
||||
MinCharacters="2"/>
|
||||
</MudStack>
|
||||
|
||||
@if(SelectedTarget != null)
|
||||
{
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.subtitle2" Class="font-weight-bold">@L["Compilations to Merge"]</MudText>
|
||||
<MudText Typo="Typo.caption">
|
||||
@L["Search and add one or more compilations to merge into the target. The selected compilations will be deleted after merging their data."]
|
||||
</MudText>
|
||||
|
||||
<MudAutocomplete T="SoftwareCompilationDto"
|
||||
Value="_sourcePickerValue"
|
||||
ValueChanged="OnSourcePicked"
|
||||
SearchFunc="SearchSourcesAsync"
|
||||
ToStringFunc="@(c => c?.Name ?? string.Empty)"
|
||||
Label="@L["Search compilation to add"]"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
DebounceInterval="300"
|
||||
MinCharacters="2"/>
|
||||
|
||||
@if(SelectedSources.Count > 0)
|
||||
{
|
||||
<MudChipSet T="string" ReadOnly="false">
|
||||
@foreach(var source in SelectedSources)
|
||||
{
|
||||
<MudChip T="string" Value="@source.Name" OnClose="@(() => RemoveSource(source))">@source.Name</MudChip>
|
||||
}
|
||||
</MudChipSet>
|
||||
|
||||
<MudAlert Severity="Severity.Warning">
|
||||
@L["Will merge"] @SelectedSources.Count @L["compilation(s) into"] "<strong>@SelectedTarget.Name</strong>".
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">@L["Cancel"]</MudButton>
|
||||
<MudButton Color="Color.Warning"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Merge"
|
||||
Disabled="@(SelectedTarget == null || SelectedSources.Count == 0)">
|
||||
@L["Merge Compilations"]
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -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<SoftwareCompilationDto> SelectedSources { get; set; } = [];
|
||||
SoftwareCompilationDto _sourcePickerValue;
|
||||
|
||||
async Task<IEnumerable<SoftwareCompilationDto>> SearchTargetAsync(string value, CancellationToken ct)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(value)) return [];
|
||||
|
||||
return await SoftwareCompilationsService.GetPagedAsync(0, 20, value);
|
||||
}
|
||||
|
||||
async Task<IEnumerable<SoftwareCompilationDto>> SearchSourcesAsync(string value, CancellationToken ct)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(value)) return [];
|
||||
|
||||
List<SoftwareCompilationDto> 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Marechai.Pages.Admin;
|
||||
|
||||
public class MergeSoftwareCompilationsDialogResult
|
||||
{
|
||||
public ulong TargetId { get; set; }
|
||||
public List<ulong> SourceIds { get; set; }
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
<MudPaper Elevation="2" Class="rounded-lg">
|
||||
<MudToolBar Dense="true">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="OpenAddDialog">@L["Add Compilation"]</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Merge" OnClick="OpenMergeDialog" Class="ml-2">@L["Merge Compilations"]</MudButton>
|
||||
<MudSpacer/>
|
||||
<MudTextField T="string" ValueChanged="OnSearch" Placeholder="@L["Search by name..."]"
|
||||
Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
|
||||
@@ -142,6 +142,33 @@ public partial class SoftwareCompilations
|
||||
}
|
||||
}
|
||||
|
||||
async Task OpenMergeDialog()
|
||||
{
|
||||
IDialogReference dialog = await DialogService.ShowAsync<MergeSoftwareCompilationsDialog>(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<DeleteConfirmDialog> parameters = new()
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<root xmlns:ns1="urn:schemas-microsoft-com:xml-msdata" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:schema id="root">
|
||||
<xs:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xs:element name="root" ns1:IsDataSet="true">
|
||||
<xs:complexType>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="metadata">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" use="required" type="xsd:string" />
|
||||
<xs:attribute name="type" type="xsd:string" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="assembly">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="alias" type="xsd:string" />
|
||||
<xs:attribute name="name" type="xsd:string" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="data">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
<xs:element name="comment" type="xsd:string" minOccurs="0" ns1:Ordinal="2" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" ns1:Ordinal="1" />
|
||||
<xs:attribute name="type" type="xsd:string" ns1:Ordinal="3" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" ns1:Ordinal="4" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="resheader">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="Merge Compilations" xml:space="preserve">
|
||||
<value>Kompilationen zusammenführen</value>
|
||||
</data>
|
||||
<data name="Compilations merged successfully." xml:space="preserve">
|
||||
<value>Kompilationen erfolgreich zusammengeführt.</value>
|
||||
</data>
|
||||
<data name="Target Compilation" xml:space="preserve">
|
||||
<value>Zielkompilation</value>
|
||||
</data>
|
||||
<data name="Select target compilation" xml:space="preserve">
|
||||
<value>Zielkompilation auswählen</value>
|
||||
</data>
|
||||
<data name="Compilations to Merge" xml:space="preserve">
|
||||
<value>Zu fusionierende Kompilationen</value>
|
||||
</data>
|
||||
<data name="Search and add one or more compilations to merge into the target. The selected compilations will be deleted after merging their data." xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Will merge" xml:space="preserve">
|
||||
<value>Wird zusammenführen</value>
|
||||
</data>
|
||||
<data name="compilation(s) into" xml:space="preserve">
|
||||
<value>Kompilation(en) in</value>
|
||||
</data>
|
||||
<data name="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." xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Search compilation to add" xml:space="preserve">
|
||||
<value>Kompilation zum Hinzufügen suchen</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<root xmlns:ns1="urn:schemas-microsoft-com:xml-msdata" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:schema id="root">
|
||||
<xs:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xs:element name="root" ns1:IsDataSet="true">
|
||||
<xs:complexType>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="metadata">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" use="required" type="xsd:string" />
|
||||
<xs:attribute name="type" type="xsd:string" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="assembly">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="alias" type="xsd:string" />
|
||||
<xs:attribute name="name" type="xsd:string" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="data">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
<xs:element name="comment" type="xsd:string" minOccurs="0" ns1:Ordinal="2" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" ns1:Ordinal="1" />
|
||||
<xs:attribute name="type" type="xsd:string" ns1:Ordinal="3" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" ns1:Ordinal="4" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="resheader">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="Merge Compilations" xml:space="preserve">
|
||||
<value>Fusionar compilaciones</value>
|
||||
</data>
|
||||
<data name="Compilations merged successfully." xml:space="preserve">
|
||||
<value>Compilaciones fusionadas correctamente.</value>
|
||||
</data>
|
||||
<data name="Target Compilation" xml:space="preserve">
|
||||
<value>Compilación de destino</value>
|
||||
</data>
|
||||
<data name="Select target compilation" xml:space="preserve">
|
||||
<value>Seleccionar compilación de destino</value>
|
||||
</data>
|
||||
<data name="Compilations to Merge" xml:space="preserve">
|
||||
<value>Compilaciones a fusionar</value>
|
||||
</data>
|
||||
<data name="Search and add one or more compilations to merge into the target. The selected compilations will be deleted after merging their data." xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Will merge" xml:space="preserve">
|
||||
<value>Se fusionarán</value>
|
||||
</data>
|
||||
<data name="compilation(s) into" xml:space="preserve">
|
||||
<value>compilación(es) en</value>
|
||||
</data>
|
||||
<data name="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." xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Search compilation to add" xml:space="preserve">
|
||||
<value>Buscar compilación para añadir</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<root xmlns:ns1="urn:schemas-microsoft-com:xml-msdata" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:schema id="root">
|
||||
<xs:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xs:element name="root" ns1:IsDataSet="true">
|
||||
<xs:complexType>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="metadata">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" use="required" type="xsd:string" />
|
||||
<xs:attribute name="type" type="xsd:string" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="assembly">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="alias" type="xsd:string" />
|
||||
<xs:attribute name="name" type="xsd:string" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="data">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
<xs:element name="comment" type="xsd:string" minOccurs="0" ns1:Ordinal="2" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" ns1:Ordinal="1" />
|
||||
<xs:attribute name="type" type="xsd:string" ns1:Ordinal="3" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" ns1:Ordinal="4" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="resheader">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="Merge Compilations" xml:space="preserve">
|
||||
<value>Fusionner les compilations</value>
|
||||
</data>
|
||||
<data name="Compilations merged successfully." xml:space="preserve">
|
||||
<value>Compilations fusionnées avec succès.</value>
|
||||
</data>
|
||||
<data name="Target Compilation" xml:space="preserve">
|
||||
<value>Compilation cible</value>
|
||||
</data>
|
||||
<data name="Select target compilation" xml:space="preserve">
|
||||
<value>Sélectionner la compilation cible</value>
|
||||
</data>
|
||||
<data name="Compilations to Merge" xml:space="preserve">
|
||||
<value>Compilations à fusionner</value>
|
||||
</data>
|
||||
<data name="Search and add one or more compilations to merge into the target. The selected compilations will be deleted after merging their data." xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Will merge" xml:space="preserve">
|
||||
<value>Fusionnera</value>
|
||||
</data>
|
||||
<data name="compilation(s) into" xml:space="preserve">
|
||||
<value>compilation(s) dans</value>
|
||||
</data>
|
||||
<data name="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." xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Search compilation to add" xml:space="preserve">
|
||||
<value>Rechercher une compilation à ajouter</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<root xmlns:ns1="urn:schemas-microsoft-com:xml-msdata" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:schema id="root">
|
||||
<xs:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xs:element name="root" ns1:IsDataSet="true">
|
||||
<xs:complexType>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="metadata">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" use="required" type="xsd:string" />
|
||||
<xs:attribute name="type" type="xsd:string" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="assembly">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="alias" type="xsd:string" />
|
||||
<xs:attribute name="name" type="xsd:string" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="data">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
<xs:element name="comment" type="xsd:string" minOccurs="0" ns1:Ordinal="2" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" ns1:Ordinal="1" />
|
||||
<xs:attribute name="type" type="xsd:string" ns1:Ordinal="3" />
|
||||
<xs:attribute name="mimetype" type="xsd:string" ns1:Ordinal="4" />
|
||||
<xs:attribute ref="xml:space" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="resheader">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xsd:string" use="required" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="Merge Compilations" xml:space="preserve">
|
||||
<value>Unisci compilazioni</value>
|
||||
</data>
|
||||
<data name="Compilations merged successfully." xml:space="preserve">
|
||||
<value>Compilazioni unite con successo.</value>
|
||||
</data>
|
||||
<data name="Target Compilation" xml:space="preserve">
|
||||
<value>Compilazione di destinazione</value>
|
||||
</data>
|
||||
<data name="Select target compilation" xml:space="preserve">
|
||||
<value>Seleziona compilazione di destinazione</value>
|
||||
</data>
|
||||
<data name="Compilations to Merge" xml:space="preserve">
|
||||
<value>Compilazioni da unire</value>
|
||||
</data>
|
||||
<data name="Search and add one or more compilations to merge into the target. The selected compilations will be deleted after merging their data." xml:space="preserve">
|
||||
<value>Cerca e aggiungi una o più compilazioni da unire alla destinazione. Le compilazioni selezionate verranno eliminate dopo l'unione dei loro dati.</value>
|
||||
</data>
|
||||
<data name="Will merge" xml:space="preserve">
|
||||
<value>Verranno unite</value>
|
||||
</data>
|
||||
<data name="compilation(s) into" xml:space="preserve">
|
||||
<value>compilazione/i in</value>
|
||||
</data>
|
||||
<data name="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." xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Search compilation to add" xml:space="preserve">
|
||||
<value>Cerca compilazione da aggiungere</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -137,6 +137,30 @@ public class SoftwareCompilationsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool succeeded, string error)> MergeAsync(ulong targetId, List<ulong> 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<List<SoftwareReleaseDto>> GetReleasesAsync(int id)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user