180 lines
6.4 KiB
C#
180 lines
6.4 KiB
C#
using System.Collections.ObjectModel;
|
|
using ClaudeDo.Data;
|
|
using ClaudeDo.Ui.Localization;
|
|
using ClaudeDo.Ui.Services;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
|
|
|
/// <summary>One fallback-list choice; Id null = no fallback.</summary>
|
|
public sealed record FallbackListOption(string? Id, string Name);
|
|
|
|
public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
|
{
|
|
/// <summary>
|
|
/// Der Aus-Schalter der gesamten Ticketsystem-Anbindung in der UI, wie bei
|
|
/// <c>SettingsModalViewModel.ShowOnlineInbox</c>. Er versteckt drei Einstiegspunkte: diesen
|
|
/// Settings-Tab, das Ticket-Projekt-Dropdown in den Listen-Einstellungen und den
|
|
/// Kontextmenü-Eintrag zum Importieren.
|
|
///
|
|
/// Pausiert am 2026-08-27 (API unter dem Hostnamen nicht erreichbar), reaktiviert am
|
|
/// 2026-09-01: die API läuft unter <c>http://192.168.45.161</c> und alle vier Routen sind
|
|
/// live verifiziert. Base-URL ist deshalb eine IP, kein Hostname — <c>bandelapis.fb-tuning.local</c>
|
|
/// lehnt die Verbindung weiterhin ab.
|
|
/// </summary>
|
|
public const bool FeatureEnabled = true;
|
|
|
|
private readonly IWorkerClient _worker;
|
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
|
|
[ObservableProperty] private string _apiBaseUrl = "";
|
|
[ObservableProperty] private string _token = "";
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(TokenPlaceholder))]
|
|
private bool _tokenSet;
|
|
[ObservableProperty] private bool _isBusy;
|
|
[ObservableProperty] private string _statusMessage = "";
|
|
|
|
// Never shows the stored token — TicketSettingsDto only ever carries the bool.
|
|
public string TokenPlaceholder => TokenSet ? Loc.T("settings.tickets.tokenSet") : "";
|
|
|
|
/// <summary>Azure DevOps PAT being typed; never shows the stored one.</summary>
|
|
[ObservableProperty] private string _azdoToken = "";
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(AzdoTokenPlaceholder))]
|
|
private bool _azdoTokenSet;
|
|
[ObservableProperty] private string _azdoStatusMessage = "";
|
|
|
|
public string AzdoTokenPlaceholder => AzdoTokenSet ? Loc.T("settings.tickets.tokenSet") : "";
|
|
|
|
public ObservableCollection<FallbackListOption> FallbackLists { get; } = new();
|
|
[ObservableProperty] private FallbackListOption? _selectedFallbackList;
|
|
|
|
// Only a successful load may write the fallback back — otherwise an offline load would clear it.
|
|
private bool _fallbackLoaded;
|
|
|
|
public TicketSettingsTabViewModel(IWorkerClient worker, IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
|
{
|
|
_worker = worker;
|
|
_dbFactory = dbFactory;
|
|
}
|
|
|
|
public async Task LoadAsync()
|
|
{
|
|
AzdoTokenSet = (await _worker.GetAzureDevOpsSettingsAsync())?.TokenSet ?? false;
|
|
var dto = await _worker.GetTicketSettingsAsync();
|
|
if (dto is null)
|
|
{
|
|
StatusMessage = Loc.T("vm.tickets.workerOffline");
|
|
return;
|
|
}
|
|
ApiBaseUrl = dto.ApiBaseUrl ?? "";
|
|
TokenSet = dto.TokenSet;
|
|
|
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
|
var lists = await ctx.Lists.AsNoTracking()
|
|
.OrderBy(l => l.SortOrder).ThenBy(l => l.Name)
|
|
.Select(l => new FallbackListOption(l.Id, l.Name))
|
|
.ToListAsync();
|
|
FallbackLists.Clear();
|
|
FallbackLists.Add(new FallbackListOption(null, Loc.T("settings.tickets.fallbackNone")));
|
|
foreach (var option in lists) FallbackLists.Add(option);
|
|
SelectedFallbackList = FallbackLists.FirstOrDefault(o => o.Id == dto.FallbackListId) ?? FallbackLists[0];
|
|
_fallbackLoaded = true;
|
|
}
|
|
|
|
public async Task SaveAsync()
|
|
{
|
|
await _worker.SetTicketApiBaseUrlAsync(ApiBaseUrl);
|
|
if (!string.IsNullOrWhiteSpace(Token))
|
|
{
|
|
await _worker.SetTicketTokenAsync(Token);
|
|
Token = "";
|
|
TokenSet = true;
|
|
}
|
|
if (_fallbackLoaded)
|
|
await _worker.SetTicketFallbackListAsync(SelectedFallbackList?.Id);
|
|
if (!string.IsNullOrWhiteSpace(AzdoToken))
|
|
{
|
|
await _worker.SetAzureDevOpsTokenAsync(AzdoToken);
|
|
AzdoToken = "";
|
|
AzdoTokenSet = true;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task TestAzdoConnection()
|
|
{
|
|
IsBusy = true;
|
|
AzdoStatusMessage = "";
|
|
try
|
|
{
|
|
await SaveAsync();
|
|
var result = await _worker.TestAzureDevOpsConnectionAsync();
|
|
AzdoStatusMessage = result is { Ok: true }
|
|
? Loc.T("vm.tickets.azdoConnectionOk", result.Repo!, result.DefaultBranch!)
|
|
: result?.Error ?? Loc.T("vm.tickets.workerOffline");
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ClearAzdoToken()
|
|
{
|
|
await _worker.ClearAzureDevOpsTokenAsync();
|
|
AzdoToken = "";
|
|
AzdoTokenSet = false;
|
|
AzdoStatusMessage = Loc.T("vm.tickets.tokenCleared");
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task TestConnection()
|
|
{
|
|
IsBusy = true;
|
|
StatusMessage = "";
|
|
try
|
|
{
|
|
await SaveAsync();
|
|
var result = await _worker.TestTicketConnectionAsync();
|
|
if (result is null || !result.Ok)
|
|
{
|
|
StatusMessage = result?.Error ?? Loc.T("vm.tickets.workerOffline");
|
|
return;
|
|
}
|
|
|
|
var missing = RequiredScopes.Where(s => !result.Scopes.Contains(s)).ToList();
|
|
StatusMessage = missing.Count == 0
|
|
? Loc.T("vm.tickets.connectionOk", result.UserName!)
|
|
: Loc.T("vm.tickets.connectionMissingScopes", result.UserName!, string.Join(", ", missing));
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ClearToken()
|
|
{
|
|
await _worker.ClearTicketTokenAsync();
|
|
Token = "";
|
|
TokenSet = false;
|
|
StatusMessage = Loc.T("vm.tickets.tokenCleared");
|
|
}
|
|
|
|
// Ohne diese drei Scopes scheitert entweder der Import oder die Statusrueckmeldung —
|
|
// der mit Abstand haeufigste Konfigurationsfehler, deshalb wird er beim Testen benannt
|
|
// statt nur geloggt.
|
|
private static readonly string[] RequiredScopes =
|
|
[
|
|
"pat:projects:read",
|
|
"pat:board:read",
|
|
"pat:tickets:write",
|
|
];
|
|
}
|