From a975a03159fc35ef277a06ba303ac1c9118061b9 Mon Sep 17 00:00:00 2001 From: master <> Date: Fri, 27 Mar 2026 18:23:10 +0200 Subject: [PATCH] Replace Approve link with confirmation dialog on Releases page The "Approve (1)" action on the Releases table was a link navigating to /releases/approvals (a separate page). Users expected an inline confirmation to approve the release directly. Now shows an app-confirm-dialog with the release name, then calls the approval decision API (POST /api/v1/approvals/{id}/decision) and refreshes the release list on success. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../releases-unified-page.component.ts | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Web/StellaOps.Web/src/app/features/releases/releases-unified-page.component.ts b/src/Web/StellaOps.Web/src/app/features/releases/releases-unified-page.component.ts index 5bc171b70..f09cb37a1 100644 --- a/src/Web/StellaOps.Web/src/app/features/releases/releases-unified-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/releases/releases-unified-page.component.ts @@ -8,11 +8,15 @@ * Tab 2 "Approvals": embeds the existing ApprovalQueueComponent. */ -import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject, signal, computed } from '@angular/core'; +import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, inject, signal, computed } from '@angular/core'; import { PageActionService } from '../../core/services/page-action.service'; import { PageActionOutletComponent } from '../../shared/components/page-action-outlet/page-action-outlet.component'; import { UpperCasePipe, SlicePipe } from '@angular/common'; import { RouterLink, ActivatedRoute } from '@angular/router'; +import { take } from 'rxjs'; +import { APPROVAL_API } from '../../core/api/approval.client'; +import type { ApprovalApi } from '../../core/api/approval.client'; +import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; import { StellaFilterChipComponent, FilterChipOption } from '../../shared/components/stella-filter-chip/stella-filter-chip.component'; import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component'; import { PaginationComponent, PageChangeEvent } from '../../shared/components/pagination/pagination.component'; @@ -63,6 +67,7 @@ export interface PipelineRelease { PaginationComponent, PageActionOutletComponent, ReleaseListComponent, + ConfirmDialogComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @@ -205,10 +210,10 @@ export interface PipelineRelease { } @if (r.gatePendingApprovals > 0) { - + } @if (r.gateStatus === 'block') { @@ -260,6 +265,12 @@ export interface PipelineRelease { @if (activeTab() === 'versions') { } + + `, styles: [` @@ -380,10 +391,39 @@ export class ReleasesUnifiedPageComponent implements OnInit, OnDestroy { private readonly store = inject(ReleaseManagementStore); private readonly context = inject(PlatformContextStore); private readonly route = inject(ActivatedRoute); + private readonly approvalApi = inject(APPROVAL_API); readonly releaseTabs = RELEASE_TABS; readonly activeTab = signal(this.route.snapshot.queryParamMap.get('tab') || 'releases'); + // Approve dialog + readonly pendingApproveRelease = signal(null); + @ViewChild('approveConfirm') approveConfirmRef!: ConfirmDialogComponent; + + readonly approveMessage = computed(() => { + const r = this.pendingApproveRelease(); + if (!r) return ''; + return `Approve release "${r.name}" (${r.version || r.digest?.slice(0, 19) || 'unknown'}) for promotion?`; + }); + + openApproveDialog(r: PipelineRelease): void { + this.pendingApproveRelease.set(r); + this.approveConfirmRef.open(); + } + + executeApprove(): void { + const r = this.pendingApproveRelease(); + if (!r) return; + // Call the approval decision endpoint for this release + this.approvalApi.approve(r.id, `Approved from releases page`).pipe(take(1)).subscribe({ + next: () => { + this.pendingApproveRelease.set(null); + this.store.loadReleases({}); + }, + error: () => this.pendingApproveRelease.set(null), + }); + } + ngOnInit(): void { this.pageAction.set({ label: 'New Release', route: '/releases/new' }); this.context.initialize();