Merge branch 'claudedo/387-unit-branch-teil-3-firmen-modus-fetch-pu'
This commit is contained in:
@@ -260,6 +260,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task SetTicketTokenAsync(string token);
|
||||
Task ClearTicketTokenAsync();
|
||||
Task<TicketConnectionDto?> TestTicketConnectionAsync();
|
||||
Task<AzureDevOpsSettingsDto?> GetAzureDevOpsSettingsAsync();
|
||||
Task SetAzureDevOpsTokenAsync(string token);
|
||||
Task ClearAzureDevOpsTokenAsync();
|
||||
Task<AzureDevOpsConnectionDto?> TestAzureDevOpsConnectionAsync();
|
||||
Task<List<TicketProjectDto>> GetTicketProjectsAsync();
|
||||
/// <summary>Imports open tickets assigned to the token owner into the given list. Throws
|
||||
/// HubException (with a user-readable message) on failure so the caller can surface it via
|
||||
|
||||
@@ -791,6 +791,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public Task<TicketConnectionDto?> TestTicketConnectionAsync()
|
||||
=> TryInvokeAsync<TicketConnectionDto>("TestTicketConnection");
|
||||
|
||||
public Task<AzureDevOpsSettingsDto?> GetAzureDevOpsSettingsAsync()
|
||||
=> TryInvokeAsync<AzureDevOpsSettingsDto>("GetAzureDevOpsSettings");
|
||||
|
||||
public Task SetAzureDevOpsTokenAsync(string token)
|
||||
=> InvokeTimedAsync("SetAzureDevOpsToken", () => _hub.InvokeAsync("SetAzureDevOpsToken", token));
|
||||
|
||||
public Task ClearAzureDevOpsTokenAsync()
|
||||
=> InvokeTimedAsync("ClearAzureDevOpsToken", () => _hub.InvokeAsync("ClearAzureDevOpsToken"));
|
||||
|
||||
public Task<AzureDevOpsConnectionDto?> TestAzureDevOpsConnectionAsync()
|
||||
=> TryInvokeAsync<AzureDevOpsConnectionDto>("TestAzureDevOpsConnection");
|
||||
|
||||
public async Task<List<TicketProjectDto>> GetTicketProjectsAsync()
|
||||
=> await TryInvokeAsync<List<TicketProjectDto>>("GetTicketProjects") ?? [];
|
||||
|
||||
|
||||
@@ -676,6 +676,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Merge.SyncTaskContext(row.Id, row.Title, row.IsPlanningParent);
|
||||
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||
WorktreeStateLabel, _listWorkingDir);
|
||||
Merge.PullRequestUrl = entity.Worktree?.PullRequestUrl;
|
||||
|
||||
await AgentSettings.LoadForTaskAsync(entity, ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -728,6 +729,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
foreach (var b in targets.LocalBranches) Merge.MergeTargetBranches.Add(b);
|
||||
Merge.SelectedMergeTarget = targets.DefaultBranch;
|
||||
Merge.TargetDirty = targets.TargetDirty;
|
||||
Merge.IsCompanyRepo = targets.IsCompanyRepo;
|
||||
}
|
||||
}
|
||||
await Merge.RefreshMergePreviewAsync();
|
||||
@@ -779,6 +781,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Merge.MergeTargetBranches.Add(b);
|
||||
Merge.SelectedMergeTarget = targets.DefaultBranch;
|
||||
Merge.TargetDirty = targets.TargetDirty;
|
||||
Merge.IsCompanyRepo = targets.IsCompanyRepo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -826,6 +829,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Merge.MergeTargetBranches.Add(b);
|
||||
Merge.SelectedMergeTarget = targets.DefaultBranch;
|
||||
Merge.TargetDirty = targets.TargetDirty;
|
||||
Merge.IsCompanyRepo = targets.IsCompanyRepo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -939,6 +943,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
|
||||
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||
WorktreeStateLabel, _listWorkingDir);
|
||||
Merge.PullRequestUrl = entity.Worktree?.PullRequestUrl;
|
||||
}
|
||||
catch { /* best-effort refresh */ }
|
||||
}
|
||||
@@ -1261,7 +1266,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
if (Task is null || !_worker.IsConnected) return;
|
||||
var taskId = Task.Id;
|
||||
using var op = ApproveOp.Begin(Loc.T("ops.merge.merging"));
|
||||
// Company repo: approve = verify + push + draft PR; nothing merges locally.
|
||||
var company = Merge.IsCompanyRepo;
|
||||
using var op = ApproveOp.Begin(Loc.T(company ? "ops.merge.publishing" : "ops.merge.merging"));
|
||||
// Subscribed only for the duration of the call: the worker broadcasts to every
|
||||
// client, and a VM left on this event would keep reacting to merges for other tasks.
|
||||
void OnOperationProgress(string opKey, string phase, int current, int total)
|
||||
@@ -1276,7 +1283,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
try
|
||||
{
|
||||
var hasChildren = Subtasks.Count > 0 || ChildOutcomes.Count > 0;
|
||||
var result = await _worker.ApproveReviewAsync(taskId, Merge.SelectedMergeTarget ?? "");
|
||||
var result = await _worker.ApproveReviewAsync(taskId, company ? "" : Merge.SelectedMergeTarget ?? "");
|
||||
|
||||
if (company)
|
||||
{
|
||||
if (result?.PullRequestUrl is { } url) Merge.PullRequestUrl = url;
|
||||
else if (result?.Status == "verify_failed") ErrorReported?.Invoke(result.ErrorMessage ?? Loc.T("vm.detailsIsland.verifyFailed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing landed -- the target working dir has uncommitted changes (often another
|
||||
// session's WIP in the shared main checkout). Offer the one-shot stash retry; git
|
||||
@@ -1304,7 +1318,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (Dialogs != null)
|
||||
// Company-repo failures (no PAT, push rejected, "gepusht, PR fehlgeschlagen") go to the footer strip.
|
||||
if (company) ErrorReported?.Invoke(ex.Message);
|
||||
else if (Dialogs != null)
|
||||
await Dialogs.ShowErrorAsync(ex.Message);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -32,6 +32,22 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
||||
// dir is visible before anyone clicks, not just after the merge modal refuses it.
|
||||
[ObservableProperty] private bool _targetDirty;
|
||||
|
||||
/// <summary>Mirrors MergeTargetsDto.IsCompanyRepo: approve pushes + opens a draft PR, there is no target to pick.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowApproveMerge), nameof(ShowCreatePr), nameof(ShowUpdatePr))]
|
||||
private bool _isCompanyRepo;
|
||||
|
||||
/// <summary>The worktree's draft PR (company repo), once an approve created it.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowCreatePr), nameof(ShowUpdatePr), nameof(HasPullRequest))]
|
||||
[NotifyCanExecuteChangedFor(nameof(OpenPullRequestCommand))]
|
||||
private string? _pullRequestUrl;
|
||||
|
||||
public bool ShowApproveMerge => !IsCompanyRepo;
|
||||
public bool ShowCreatePr => IsCompanyRepo && PullRequestUrl is null;
|
||||
public bool ShowUpdatePr => IsCompanyRepo && PullRequestUrl is not null;
|
||||
public bool HasPullRequest => PullRequestUrl is not null;
|
||||
|
||||
// No CanExecute to gate here — RefreshMergePreviewAsync is triggered from the Bind path,
|
||||
// not a [RelayCommand], so PreviewOp only drives the indicator, never locks a button.
|
||||
public OperationStatus PreviewOp { get; } = new();
|
||||
@@ -146,11 +162,20 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
||||
MergeIsClean = false;
|
||||
MergeIsConflict = false;
|
||||
TargetDirty = false;
|
||||
IsCompanyRepo = false;
|
||||
PullRequestUrl = null;
|
||||
SyncWorktree(null, null, null, null, null);
|
||||
SyncTaskContext(null, null, false);
|
||||
SyncChildOutcomes(false, 0);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasPullRequest))]
|
||||
private void OpenPullRequest()
|
||||
{
|
||||
var (ok, error) = ShellOpen.Url(PullRequestUrl);
|
||||
if (!ok && error is not null) ErrorReported?.Invoke(error);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanReviewDiff))]
|
||||
private async System.Threading.Tasks.Task ReviewCombinedDiffAsync()
|
||||
{
|
||||
|
||||
@@ -40,6 +40,15 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
// 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;
|
||||
|
||||
@@ -54,6 +63,7 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
AzdoTokenSet = (await _worker.GetAzureDevOpsSettingsAsync())?.TokenSet ?? false;
|
||||
var dto = await _worker.GetTicketSettingsAsync();
|
||||
if (dto is null)
|
||||
{
|
||||
@@ -86,6 +96,40 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
}
|
||||
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]
|
||||
|
||||
@@ -321,8 +321,18 @@
|
||||
<TextBlock Classes="diff-del" Text="{Binding DiffDelText}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Company repo: no local target — approve pushes and opens a draft PR. -->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding Merge.IsCompanyRepo}">
|
||||
<TextBlock Classes="meta" Text="{loc:Tr details.workConsole.companyTargetHint}" TextWrapping="Wrap" />
|
||||
<Button Classes="btn" HorizontalAlignment="Left"
|
||||
Content="{loc:Tr details.workConsole.openPr}"
|
||||
ToolTip.Tip="{Binding Merge.PullRequestUrl}"
|
||||
Command="{Binding Merge.OpenPullRequestCommand}"
|
||||
IsVisible="{Binding Merge.HasPullRequest}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Target branch + pre-flight mergeability -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Spacing="4" IsVisible="{Binding Merge.ShowApproveMerge}">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr details.workConsole.targetBranch}" />
|
||||
<ComboBox ItemsSource="{Binding Merge.MergeTargetBranches}"
|
||||
SelectedItem="{Binding Merge.SelectedMergeTarget, Mode=TwoWay}"
|
||||
@@ -333,7 +343,8 @@
|
||||
IsVisible="{Binding Merge.TargetDirty}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="0">
|
||||
<!-- The preview is against the local target, meaningless when the PR targets origin. -->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding Merge.ShowApproveMerge}">
|
||||
<TextBlock Classes="meta" Text="{Binding Merge.MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource MossBrush}"
|
||||
IsVisible="{Binding Merge.MergeIsClean}" />
|
||||
@@ -400,8 +411,13 @@
|
||||
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<StackPanel Orientation="Horizontal" Spacing="{StaticResource SpaceSm}" Margin="0,0,8,8">
|
||||
<Button Classes="btn accent" Content="{loc:Tr details.workConsole.approveMerge}"
|
||||
Command="{Binding ApproveReviewCommand}" />
|
||||
<Button Classes="btn accent" Command="{Binding ApproveReviewCommand}">
|
||||
<Panel>
|
||||
<TextBlock Text="{loc:Tr details.workConsole.approveMerge}" IsVisible="{Binding Merge.ShowApproveMerge}" />
|
||||
<TextBlock Text="{loc:Tr details.workConsole.createDraftPr}" IsVisible="{Binding Merge.ShowCreatePr}" />
|
||||
<TextBlock Text="{loc:Tr details.workConsole.updatePr}" IsVisible="{Binding Merge.ShowUpdatePr}" />
|
||||
</Panel>
|
||||
</Button>
|
||||
<controls:OperationIndicator Status="{Binding ApproveOp}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="{StaticResource SpaceSm}" Margin="0,0,8,8">
|
||||
|
||||
@@ -738,12 +738,14 @@
|
||||
</ScrollViewer>
|
||||
</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. -->
|
||||
<!-- External services: Bandel ticket system (base URL + PAT; empty base URL disables it, so
|
||||
this tab never forces a value) and the Azure DevOps PAT for company-repo push + draft PR. -->
|
||||
<TabItem Header="{loc:Tr settings.sidebar.categoryTickets}">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="14" Margin="0,8,0,0">
|
||||
|
||||
<TextBlock Classes="section-label" Text="{loc:Tr settings.tickets.ticketSection}"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.tickets.baseUrl}"/>
|
||||
<TextBox Text="{Binding Tickets.ApiBaseUrl, Mode=TwoWay}"/>
|
||||
@@ -785,6 +787,33 @@
|
||||
<TextBlock Classes="meta" Text="{loc:Tr settings.tickets.fallbackListHint}"
|
||||
TextWrapping="Wrap" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,8,0,0"/>
|
||||
|
||||
<TextBlock Classes="section-label" Text="{loc:Tr settings.tickets.azdoSection}"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.tickets.azdoToken}"/>
|
||||
<TextBox Text="{Binding Tickets.AzdoToken, Mode=TwoWay}"
|
||||
PasswordChar="•"
|
||||
PlaceholderText="{Binding Tickets.AzdoTokenPlaceholder}"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr settings.tickets.azdoHint}"
|
||||
TextWrapping="Wrap" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="btn"
|
||||
Content="{loc:Tr settings.tickets.testConnection}"
|
||||
Command="{Binding Tickets.TestAzdoConnectionCommand}"
|
||||
IsEnabled="{Binding !Tickets.IsBusy}"/>
|
||||
<Button Classes="btn danger"
|
||||
Content="{loc:Tr settings.tickets.clearToken}"
|
||||
Command="{Binding Tickets.ClearAzdoTokenCommand}"
|
||||
IsEnabled="{Binding !Tickets.IsBusy}"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="meta" Text="{Binding Tickets.AzdoStatusMessage}" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Tickets.AzdoStatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
Reference in New Issue
Block a user