diff --git a/src/app/components/decision-list/decision-list.component.css b/src/app/components/decision-list/decision-list.component.css index 59737e5..b0fd5e5 100644 --- a/src/app/components/decision-list/decision-list.component.css +++ b/src/app/components/decision-list/decision-list.component.css @@ -64,6 +64,16 @@ border-radius: 8px; cursor: pointer; font-size: 13px; + transition: opacity 0.15s; +} + +.btn-danger:hover:not(:disabled) { + opacity: 0.82; +} + +.btn-danger:disabled { + opacity: 0.55; + cursor: not-allowed; } .decision-card { @@ -171,3 +181,129 @@ @keyframes spin { to { transform: rotate(360deg); } } + +/* ── Delete confirmation modal ───────────────────────────────────────────── */ + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 50; + background: rgba(2, 6, 23, 0.55); + display: grid; + place-items: center; + padding: 18px; + animation: backdropIn 0.18s ease; +} + +@keyframes backdropIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.delete-modal { + width: min(460px, 100%); + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 16px; + box-shadow: 0 28px 72px rgba(0, 0, 0, 0.24); + padding: 24px 24px 22px; + animation: modalIn 0.2s cubic-bezier(0.22, 1, 0.36, 1); +} + +@keyframes modalIn { + from { opacity: 0; transform: translateY(10px) scale(0.97); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +.delete-modal-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 10px; + background: #f3f4f6; + border: 1px solid #e5e7eb; + color: #374151; + margin-bottom: 14px; +} + +.delete-modal-eyebrow { + margin: 0 0 6px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; + color: #6b7280; + font-weight: 700; +} + +.delete-modal-title { + margin: 0; + font-size: 20px; + line-height: 1.3; + font-weight: 700; + color: #0a0a0a; + word-break: break-word; +} + +.delete-modal-copy { + margin: 10px 0 0; + font-size: 14px; + color: #4b5563; + line-height: 1.55; +} + +.delete-modal-error { + margin: 14px 0 0; + background: #fdecec; + color: #8d1c1c; + border: 1px solid #f7c5c5; + border-radius: 10px; + padding: 9px 12px; + font-size: 13px; + font-weight: 600; +} + +.delete-modal-actions { + margin-top: 22px; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.modal-btn { + border-radius: 10px; + border: 1px solid transparent; + padding: 9px 16px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.15s, background 0.15s; + line-height: 1; +} + +.modal-btn.cancel { + background: #fff; + color: #111; + border-color: #d1d5db; +} + +.modal-btn.cancel:hover:not(:disabled) { + background: #f9fafb; +} + +.modal-btn.confirm { + background: #111; + color: #fff; + border-color: #111; + min-width: 100px; +} + +.modal-btn.confirm:hover:not(:disabled) { + background: #000; +} + +.modal-btn:disabled { + opacity: 0.55; + cursor: not-allowed; +} diff --git a/src/app/components/decision-list/decision-list.component.html b/src/app/components/decision-list/decision-list.component.html index 687fb9c..628d28e 100644 --- a/src/app/components/decision-list/decision-list.component.html +++ b/src/app/components/decision-list/decision-list.component.html @@ -34,8 +34,64 @@

{{ decision.title }}

Edit - +
+ + + diff --git a/src/app/components/decision-list/decision-list.component.spec.ts b/src/app/components/decision-list/decision-list.component.spec.ts index f4e20f3..02b2ad3 100644 --- a/src/app/components/decision-list/decision-list.component.spec.ts +++ b/src/app/components/decision-list/decision-list.component.spec.ts @@ -66,8 +66,76 @@ describe('DecisionListComponent', () => { it('should have correct edit link', () => { const editButton = fixture.debugElement.query(By.css('.btn-secondary')); expect(editButton).toBeTruthy(); - // Since it's a relative link [decision.id, 'edit'], we check if the attribute is present or just trust the binding const link = editButton.nativeElement as HTMLAnchorElement; expect(link.textContent).toContain('Edit'); }); + + // ── Delete modal flow ──────────────────────────────────────────────────── + + it('should not show delete modal by default', () => { + const backdrop = fixture.nativeElement.querySelector('.modal-backdrop'); + expect(backdrop).toBeNull(); + }); + + it('should open delete modal with decision title when requestDeleteDecision is called', () => { + component.requestDeleteDecision(mockDecisions[0]); + fixture.detectChanges(); + + const backdrop = fixture.nativeElement.querySelector('.modal-backdrop'); + expect(backdrop).toBeTruthy(); + + const title = fixture.nativeElement.querySelector('.delete-modal-title'); + expect(title.textContent).toContain('Test Decision'); + }); + + it('should close modal and clear state when cancelDeleteDecision is called', () => { + component.requestDeleteDecision(mockDecisions[0]); + fixture.detectChanges(); + + component.cancelDeleteDecision(); + fixture.detectChanges(); + + const backdrop = fixture.nativeElement.querySelector('.modal-backdrop'); + expect(backdrop).toBeNull(); + expect(component.pendingDeleteDecision).toBeNull(); + expect(component.deleteDecisionError).toBe(''); + }); + + it('should close modal when backdrop overlay is clicked', () => { + component.requestDeleteDecision(mockDecisions[0]); + fixture.detectChanges(); + + const backdrop = fixture.nativeElement.querySelector('.modal-backdrop') as HTMLElement; + backdrop.click(); + fixture.detectChanges(); + + expect(component.pendingDeleteDecision).toBeNull(); + }); + + it('should call deleteDecision service and close modal on confirmDeleteDecision', () => { + component.requestDeleteDecision(mockDecisions[0]); + fixture.detectChanges(); + + component.confirmDeleteDecision(); + fixture.detectChanges(); + + expect(mockDecisionService.deleteDecision).toHaveBeenCalledWith('10', '1'); + expect(component.pendingDeleteDecision).toBeNull(); + expect(component.isDeletingDecision).toBeFalse(); + }); + + it('should not open modal while a delete is already in progress', () => { + component.isDeletingDecision = true; + component.requestDeleteDecision(mockDecisions[0]); + + expect(component.pendingDeleteDecision).toBeNull(); + }); + + it('should not close modal while a delete is in progress', () => { + component.requestDeleteDecision(mockDecisions[0]); + component.isDeletingDecision = true; + component.cancelDeleteDecision(); + + expect(component.pendingDeleteDecision).toEqual(mockDecisions[0]); + }); }); diff --git a/src/app/components/decision-list/decision-list.component.ts b/src/app/components/decision-list/decision-list.component.ts index 432a775..efd612f 100644 --- a/src/app/components/decision-list/decision-list.component.ts +++ b/src/app/components/decision-list/decision-list.component.ts @@ -15,6 +15,12 @@ import { Observable, Subject, takeUntil, startWith, switchMap, catchError, of } export class DecisionListComponent implements OnInit, OnDestroy { decisions$: Observable | undefined; error: string | null = null; + + // Delete confirmation state + pendingDeleteDecision: Decision | null = null; + isDeletingDecision = false; + deleteDecisionError = ''; + private refresh$ = new Subject(); private destroy$ = new Subject(); private workspaceId: string | null = null; @@ -30,7 +36,7 @@ export class DecisionListComponent implements OnInit, OnDestroy { this.decisions$ = this.refresh$.pipe( startWith(undefined), switchMap(() => this.decisionService.getDecisions(this.workspaceId!).pipe( - catchError(err => { + catchError(() => { this.error = 'Failed to load decisions. Please try again.'; return of([]); }) @@ -44,21 +50,39 @@ export class DecisionListComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - deleteDecision(id: string): void { - if (confirm('Are you sure you want to delete this decision?') && this.workspaceId) { - this.error = null; - this.decisionService.deleteDecision(this.workspaceId, id) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: () => { - this.refresh$.next(); - }, - error: (err) => { - console.error('Delete failed', err); - this.error = 'Failed to delete decision. Please try again.'; - } - }); - } + requestDeleteDecision(decision: Decision): void { + if (this.isDeletingDecision) return; + this.pendingDeleteDecision = decision; + this.deleteDecisionError = ''; + } + + cancelDeleteDecision(): void { + if (this.isDeletingDecision) return; + this.pendingDeleteDecision = null; + this.deleteDecisionError = ''; + } + + confirmDeleteDecision(): void { + const decision = this.pendingDeleteDecision; + if (!decision || !this.workspaceId || this.isDeletingDecision) return; + + this.isDeletingDecision = true; + this.deleteDecisionError = ''; + + this.decisionService.deleteDecision(this.workspaceId, decision.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + this.pendingDeleteDecision = null; + this.isDeletingDecision = false; + this.error = null; + this.refresh$.next(); + }, + error: () => { + this.isDeletingDecision = false; + this.deleteDecisionError = 'Unable to delete decision. Please try again.'; + } + }); } isOverdue(decision: Decision): boolean {