feat(ui): Settings-Tab fuer die Ticketsystem-Anbindung
This commit is contained in:
@@ -14,7 +14,8 @@
|
|||||||
"categoryWorktrees": "Worktrees",
|
"categoryWorktrees": "Worktrees",
|
||||||
"categoryFiles": "Dateien & Prompts",
|
"categoryFiles": "Dateien & Prompts",
|
||||||
"categorySkills": "Session Skills",
|
"categorySkills": "Session Skills",
|
||||||
"categoryReports": "Berichte"
|
"categoryReports": "Berichte",
|
||||||
|
"categoryTickets": "Ticketsystem"
|
||||||
},
|
},
|
||||||
"repoHint": {
|
"repoHint": {
|
||||||
"text": "Wirkt erst mit verknüpftem Repo.",
|
"text": "Wirkt erst mit verknüpftem Repo.",
|
||||||
@@ -132,6 +133,14 @@
|
|||||||
"configSection": "KONFIGURATION",
|
"configSection": "KONFIGURATION",
|
||||||
"saveButton": "Konfiguration speichern"
|
"saveButton": "Konfiguration speichern"
|
||||||
},
|
},
|
||||||
|
"tickets": {
|
||||||
|
"baseUrl": "API-Base-URL",
|
||||||
|
"baseUrlHint": "Leer lassen, um die Ticket-Anbindung komplett zu deaktivieren.",
|
||||||
|
"token": "Personal Access Token",
|
||||||
|
"tokenSet": "gespeichert",
|
||||||
|
"testConnection": "Verbindung testen",
|
||||||
|
"clearToken": "Token entfernen"
|
||||||
|
},
|
||||||
"inherit": {
|
"inherit": {
|
||||||
"inheritedFromList": "geerbt · Liste",
|
"inheritedFromList": "geerbt · Liste",
|
||||||
"inheritedFromGlobal": "geerbt · Global",
|
"inheritedFromGlobal": "geerbt · Global",
|
||||||
@@ -726,6 +735,7 @@
|
|||||||
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Wird zusammengeführt…", "progressVerifying": "Verify-Kommando der Liste läuft… ({0})", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "blocked": "Blockiert: {0}", "verifyFailed": "Merge ist gelandet, aber das Verify-Kommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf Erledigt gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Wird zusammengeführt…", "progressVerifying": "Verify-Kommando der Liste läuft… ({0})", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "blocked": "Blockiert: {0}", "verifyFailed": "Merge ist gelandet, aber das Verify-Kommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf Erledigt gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
||||||
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
||||||
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
|
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
|
||||||
|
"tickets": { "connectionOk": "Verbunden als {0}.", "connectionMissingScopes": "Verbunden als {0}, aber es fehlen Scopes: {1}", "tokenCleared": "Token entfernt.", "workerOffline": "Worker nicht erreichbar." },
|
||||||
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "error": "Fehler: {0}" },
|
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "error": "Fehler: {0}" },
|
||||||
"usageMonitor": {
|
"usageMonitor": {
|
||||||
"loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}",
|
"loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}",
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"categoryWorktrees": "Worktrees",
|
"categoryWorktrees": "Worktrees",
|
||||||
"categoryFiles": "Files & Prompts",
|
"categoryFiles": "Files & Prompts",
|
||||||
"categorySkills": "Session Skills",
|
"categorySkills": "Session Skills",
|
||||||
"categoryReports": "Reports"
|
"categoryReports": "Reports",
|
||||||
|
"categoryTickets": "Ticket system"
|
||||||
},
|
},
|
||||||
"repoHint": {
|
"repoHint": {
|
||||||
"text": "Only takes effect once a repo is linked.",
|
"text": "Only takes effect once a repo is linked.",
|
||||||
@@ -132,6 +133,14 @@
|
|||||||
"configSection": "CONFIGURATION",
|
"configSection": "CONFIGURATION",
|
||||||
"saveButton": "Save config"
|
"saveButton": "Save config"
|
||||||
},
|
},
|
||||||
|
"tickets": {
|
||||||
|
"baseUrl": "API base URL",
|
||||||
|
"baseUrlHint": "Leave empty to disable the ticket integration entirely.",
|
||||||
|
"token": "Personal access token",
|
||||||
|
"tokenSet": "stored",
|
||||||
|
"testConnection": "Test connection",
|
||||||
|
"clearToken": "Remove token"
|
||||||
|
},
|
||||||
"inherit": {
|
"inherit": {
|
||||||
"inheritedFromList": "inherited · List",
|
"inheritedFromList": "inherited · List",
|
||||||
"inheritedFromGlobal": "inherited · Global",
|
"inheritedFromGlobal": "inherited · Global",
|
||||||
@@ -726,6 +735,7 @@
|
|||||||
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Merging…", "progressVerifying": "Running the list's verify command… ({0})", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "blocked": "Blocked: {0}", "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Merging…", "progressVerifying": "Running the list's verify command… ({0})", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "blocked": "Blocked: {0}", "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
||||||
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
||||||
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
|
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
|
||||||
|
"tickets": { "connectionOk": "Connected as {0}.", "connectionMissingScopes": "Connected as {0}, but missing scopes: {1}", "tokenCleared": "Token removed.", "workerOffline": "Worker not reachable." },
|
||||||
"weeklyReport": { "invalidRange": "Invalid date range.", "error": "Error: {0}" },
|
"weeklyReport": { "invalidRange": "Invalid date range.", "error": "Error: {0}" },
|
||||||
"usageMonitor": {
|
"usageMonitor": {
|
||||||
"loadFailed": "Couldn't load usage data: {0}",
|
"loadFailed": "Couldn't load usage data: {0}",
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
|
|
||||||
|
public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IWorkerClient _worker;
|
||||||
|
|
||||||
|
[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") : "";
|
||||||
|
|
||||||
|
public TicketSettingsTabViewModel(IWorkerClient worker) => _worker = worker;
|
||||||
|
|
||||||
|
public async Task LoadAsync()
|
||||||
|
{
|
||||||
|
var dto = await _worker.GetTicketSettingsAsync();
|
||||||
|
if (dto is null)
|
||||||
|
{
|
||||||
|
StatusMessage = Loc.T("vm.tickets.workerOffline");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApiBaseUrl = dto.ApiBaseUrl ?? "";
|
||||||
|
TokenSet = dto.TokenSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
await _worker.SetTicketApiBaseUrlAsync(ApiBaseUrl);
|
||||||
|
if (!string.IsNullOrWhiteSpace(Token))
|
||||||
|
{
|
||||||
|
await _worker.SetTicketTokenAsync(Token);
|
||||||
|
Token = "";
|
||||||
|
TokenSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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",
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
public PrimeClaudeTabViewModel Prime { get; }
|
public PrimeClaudeTabViewModel Prime { get; }
|
||||||
public OnlineInboxSettingsViewModel OnlineInbox { get; }
|
public OnlineInboxSettingsViewModel OnlineInbox { get; }
|
||||||
public SessionSkillsSettingsTabViewModel SessionSkills { get; }
|
public SessionSkillsSettingsTabViewModel SessionSkills { get; }
|
||||||
|
public TicketSettingsTabViewModel Tickets { get; }
|
||||||
|
|
||||||
// Online Inbox ist unfertig und vor Usern verborgen — hier auf true für Reaktivierung.
|
// Online Inbox ist unfertig und vor Usern verborgen — hier auf true für Reaktivierung.
|
||||||
public bool ShowOnlineInbox => false;
|
public bool ShowOnlineInbox => false;
|
||||||
@@ -106,6 +107,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
Prime = prime;
|
Prime = prime;
|
||||||
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
|
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
|
||||||
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
|
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
|
||||||
|
Tickets = new TicketSettingsTabViewModel(worker);
|
||||||
|
|
||||||
_categories = new List<SettingsCategory>
|
_categories = new List<SettingsCategory>
|
||||||
{
|
{
|
||||||
@@ -117,6 +119,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
new("SessionSkills", new LocalizedString(localizer, "settings.sidebar.categorySkills"), "Erweitert", true, true),
|
new("SessionSkills", new LocalizedString(localizer, "settings.sidebar.categorySkills"), "Erweitert", true, true),
|
||||||
new("Reports", new LocalizedString(localizer, "settings.sidebar.categoryReports"), "Erweitert", true, true),
|
new("Reports", new LocalizedString(localizer, "settings.sidebar.categoryReports"), "Erweitert", true, true),
|
||||||
new("OnlineInbox", new LocalizedString(localizer, "settings.onlineInbox.tabHeader"), "Erweitert", ShowOnlineInbox, false),
|
new("OnlineInbox", new LocalizedString(localizer, "settings.onlineInbox.tabHeader"), "Erweitert", ShowOnlineInbox, false),
|
||||||
|
new("Tickets", new LocalizedString(localizer, "settings.sidebar.categoryTickets"), "Erweitert", true, false),
|
||||||
};
|
};
|
||||||
BasisCategories = _categories.Where(c => c.Group == "Basis").ToList();
|
BasisCategories = _categories.Where(c => c.Group == "Basis").ToList();
|
||||||
ErweitertCategories = _categories.Where(c => c.Group == "Erweitert").ToList();
|
ErweitertCategories = _categories.Where(c => c.Group == "Erweitert").ToList();
|
||||||
@@ -167,6 +170,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
await Prime.LoadAsync();
|
await Prime.LoadAsync();
|
||||||
await OnlineInbox.LoadAsync();
|
await OnlineInbox.LoadAsync();
|
||||||
await SessionSkills.LoadAsync(dto?.SessionSkills);
|
await SessionSkills.LoadAsync(dto?.SessionSkills);
|
||||||
|
await Tickets.LoadAsync();
|
||||||
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
||||||
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -213,6 +217,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
await _worker.UpdateAppSettingsAsync(dto);
|
await _worker.UpdateAppSettingsAsync(dto);
|
||||||
await Prime.SaveAsync();
|
await Prime.SaveAsync();
|
||||||
await OnlineInbox.SaveAsync();
|
await OnlineInbox.SaveAsync();
|
||||||
|
await Tickets.SaveAsync();
|
||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); }
|
catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); }
|
||||||
|
|||||||
@@ -690,6 +690,45 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Ticketsystem: Base-URL + PAT for the Bandel ticket integration. Empty base URL
|
||||||
|
disables the feature entirely, so this tab intentionally never forces a value. -->
|
||||||
|
<TabItem Header="{loc:Tr settings.sidebar.categoryTickets}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="14" Margin="0,8,0,0">
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.tickets.baseUrl}"/>
|
||||||
|
<TextBox Text="{Binding Tickets.ApiBaseUrl, Mode=TwoWay}"/>
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr settings.tickets.baseUrlHint}"
|
||||||
|
TextWrapping="Wrap" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.tickets.token}"/>
|
||||||
|
<TextBox Text="{Binding Tickets.Token, Mode=TwoWay}"
|
||||||
|
PasswordChar="•"
|
||||||
|
PlaceholderText="{Binding Tickets.TokenPlaceholder}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Classes="btn"
|
||||||
|
Content="{loc:Tr settings.tickets.testConnection}"
|
||||||
|
Command="{Binding Tickets.TestConnectionCommand}"
|
||||||
|
IsEnabled="{Binding !Tickets.IsBusy}"/>
|
||||||
|
<Button Classes="btn danger"
|
||||||
|
Content="{loc:Tr settings.tickets.clearToken}"
|
||||||
|
Command="{Binding Tickets.ClearTokenCommand}"
|
||||||
|
IsEnabled="{Binding !Tickets.IsBusy}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Classes="meta" Text="{Binding Tickets.StatusMessage}"
|
||||||
|
IsVisible="{Binding Tickets.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
Reference in New Issue
Block a user