From 1e375297fc4cca7819d58ec4e908068eacf8d8ad Mon Sep 17 00:00:00 2001 From: Neethika Date: Mon, 13 Apr 2026 14:58:29 -0400 Subject: [PATCH] feat(dashboard): improve workspace actions and deletion UX --- src/app/components/dashboard/dashboard.css | 109 ++++++++++++++++++ src/app/components/dashboard/dashboard.html | 39 ++++++- .../components/dashboard/dashboard.spec.ts | 32 ++++- src/app/components/dashboard/dashboard.ts | 81 ++++++++++--- 4 files changed, 238 insertions(+), 23 deletions(-) diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index 722f339..57ac088 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -155,6 +155,26 @@ opacity: 0.92; } +.workspace-feedback { + margin: -20px 0 18px; + border-radius: 10px; + padding: 10px 12px; + font-size: 13px; + font-weight: 600; +} + +.workspace-feedback.success { + background: #e7f7ed; + color: #1f6b3d; + border: 1px solid #b7dfc4; +} + +.workspace-feedback.error { + background: #fdecec; + color: #8d1c1c; + border: 1px solid #f7c5c5; +} + .workspace-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); @@ -237,6 +257,95 @@ background: #000; } +.delete-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 40; + background: rgba(2, 6, 23, 0.62); + display: grid; + place-items: center; + padding: 18px; +} + +.delete-modal { + width: min(480px, 100%); + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.16); + border-radius: 16px; + box-shadow: 0 26px 64px rgba(0, 0, 0, 0.28); + padding: 22px; +} + +.delete-modal-eyebrow { + margin: 0 0 8px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; + color: #6b7280; + font-weight: 700; +} + +.delete-modal h3 { + margin: 0; + font-size: 24px; + line-height: 1.25; +} + +.delete-modal-copy { + margin: 10px 0 0; + color: #4b5563; + line-height: 1.5; +} + +.delete-modal-error { + margin: 12px 0 0; + background: #fdecec; + color: #8d1c1c; + border: 1px solid #f7c5c5; + border-radius: 10px; + padding: 9px 11px; + font-size: 13px; + font-weight: 600; +} + +.delete-modal-actions { + margin-top: 18px; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.modal-btn { + border-radius: 10px; + border: 1px solid transparent; + padding: 9px 14px; + font-size: 13px; + font-weight: 600; + cursor: pointer; +} + +.modal-btn.cancel { + background: #fff; + color: #111; + border-color: #d1d5db; +} + +.modal-btn.confirm { + background: #111; + color: #fff; + border-color: #111; +} + +.modal-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + .empty-state { border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 281ace7..16775f5 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -20,7 +20,7 @@

Decision Workspace Dashboard

-

Track active workspaces, Slack conversations, and GitHub work in one place with clean structure and minimal noise.

+

Track active workspaces, Slack conversations, GitHub updates, and JIRA work in one place with clean structure and minimal noise.

@@ -32,6 +32,8 @@

Decision Workspace Dashboard

Your Workspaces

Create Workspace +

{{ deleteWorkspaceSuccess }}

+

{{ deleteWorkspaceError }}

@@ -40,8 +42,15 @@

{{ ws.name }}

Created: {{ ws.createdDate | date: 'mediumDate' }}
Open - Edit - + Edit +
@@ -54,6 +63,25 @@

No workspaces yet

+ +
@@ -64,11 +92,12 @@

Signals

+
-

Slack messages and GitHub updates appear here as action-ready signals.

+

Slack messages, GitHub updates, and JIRA work appear here as action-ready signals.

Signals

No matching signals

-

Connect Slack or GitHub in workspace integrations to start pulling messages and assigned work into Sentinent.

+

Connect Slack, GitHub, or JIRA in workspace integrations to start pulling messages and assigned work into Sentinent.

diff --git a/src/app/components/dashboard/dashboard.spec.ts b/src/app/components/dashboard/dashboard.spec.ts index e2b3827..e5e785f 100644 --- a/src/app/components/dashboard/dashboard.spec.ts +++ b/src/app/components/dashboard/dashboard.spec.ts @@ -19,7 +19,8 @@ describe('Dashboard', () => { mockWorkspaceService = { getWorkspaces: jasmine.createSpy('getWorkspaces').and.returnValue(of([ { id: '1', name: 'Test Workspace', description: 'Test Desc', createdDate: new Date(), ownerId: 'u1' } - ])) + ])), + deleteWorkspace: jasmine.createSpy('deleteWorkspace').and.returnValue(of(true)) }; mockAuthService = { @@ -92,7 +93,7 @@ describe('Dashboard', () => { expect(compiled.querySelector('.workspace-card h3')?.textContent).toContain('Test Workspace'); }); - it('should hide edit action for workspaces not owned by the current user', () => { + it('should show edit action for all workspace cards', () => { mockWorkspaceService.getWorkspaces.and.returnValue(of([ { id: '1', name: 'Owned Workspace', description: 'Owned Desc', createdDate: new Date(), ownerId: 'u1' }, { id: '2', name: 'Shared Workspace', description: 'Shared Desc', createdDate: new Date(), ownerId: 'u2' } @@ -107,7 +108,7 @@ describe('Dashboard', () => { const sharedCard = cards.find(card => card.textContent?.includes('Shared Workspace')); expect(ownedCard?.textContent).toContain('Edit'); - expect(sharedCard?.textContent).not.toContain('Edit'); + expect(sharedCard?.textContent).toContain('Edit'); }); it('should call logout on button click', () => { @@ -126,4 +127,29 @@ describe('Dashboard', () => { expect(compiled.textContent).toContain('Slack'); expect(compiled.textContent).toContain('Test Slack Signal'); }); + + it('should render jira source filter', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('JIRA'); + }); + + it('should open custom delete dialog from workspace card', () => { + const deleteButton = fixture.nativeElement.querySelector('.delete-btn') as HTMLButtonElement; + deleteButton.click(); + fixture.detectChanges(); + + const dialog = fixture.nativeElement.querySelector('.delete-modal') as HTMLElement | null; + expect(dialog).not.toBeNull(); + expect(dialog?.textContent).toContain('Delete "Test Workspace"?'); + }); + + it('should delete workspace via custom dialog and remove it from list', () => { + component.requestDeleteWorkspace(component.workspaces[0]); + component.confirmDeleteWorkspace(); + fixture.detectChanges(); + + expect(mockWorkspaceService.deleteWorkspace).toHaveBeenCalledWith('1'); + expect(component.workspaces.length).toBe(0); + expect(component.pendingDeleteWorkspace).toBeNull(); + }); }); diff --git a/src/app/components/dashboard/dashboard.ts b/src/app/components/dashboard/dashboard.ts index 7909b63..3a9c10b 100644 --- a/src/app/components/dashboard/dashboard.ts +++ b/src/app/components/dashboard/dashboard.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { AuthService } from '../../services/auth'; import { WorkspaceService } from '../../services/workspace'; @@ -16,7 +16,7 @@ import { SearchBarComponent } from '../search-bar/search-bar'; templateUrl: './dashboard.html', styleUrl: './dashboard.css', }) -export class Dashboard implements OnInit { +export class Dashboard implements OnInit, OnDestroy { private authService = inject(AuthService); private workspaceService = inject(WorkspaceService); private signalService = inject(SignalService); @@ -29,11 +29,13 @@ export class Dashboard implements OnInit { filters: SignalFilters = { source: 'all', status: 'all' }; githubBanner = ''; slackBanner = ''; - currentUserId: string | null = null; + pendingDeleteWorkspace: Workspace | null = null; + isDeletingWorkspace = false; + deleteWorkspaceError = ''; + deleteWorkspaceSuccess = ''; + private deleteNoticeTimeoutId?: ReturnType; ngOnInit() { - this.currentUserId = this.authService.getCurrentUserId(); - this.workspaceService.getWorkspaces().subscribe({ next: ws => { this.workspaces = ws; @@ -60,22 +62,57 @@ export class Dashboard implements OnInit { this.loadSignals(); } - canEditWorkspace(workspace: Workspace): boolean { - return this.currentUserId !== null && workspace.ownerId === this.currentUserId; + ngOnDestroy(): void { + if (this.deleteNoticeTimeoutId !== undefined) { + clearTimeout(this.deleteNoticeTimeoutId); + this.deleteNoticeTimeoutId = undefined; + } + } + + requestDeleteWorkspace(workspace: Workspace): void { + if (this.isDeletingWorkspace) { + return; + } + this.pendingDeleteWorkspace = workspace; + this.deleteWorkspaceError = ''; } - deleteWorkspace(workspace: Workspace) { - const confirmed = window.confirm(`Delete workspace "${workspace.name}"?`); - if (!confirmed) { + cancelDeleteWorkspace(): void { + if (this.isDeletingWorkspace) { return; } + this.pendingDeleteWorkspace = null; + this.deleteWorkspaceError = ''; + } + + confirmDeleteWorkspace(): void { + const workspace = this.pendingDeleteWorkspace; + if (!workspace || this.isDeletingWorkspace) { + return; + } + + this.isDeletingWorkspace = true; + this.deleteWorkspaceError = ''; - this.workspaceService.deleteWorkspace(workspace.id).subscribe(deleted => { - if (!deleted) { - return; + this.workspaceService.deleteWorkspace(workspace.id).subscribe({ + next: (deleted) => { + if (!deleted) { + this.isDeletingWorkspace = false; + this.deleteWorkspaceError = 'Unable to delete workspace. Please try again.'; + return; + } + + this.workspaces = this.workspaces.filter(ws => ws.id !== workspace.id); + this.pendingDeleteWorkspace = null; + this.isDeletingWorkspace = false; + this.showDeleteSuccess(`Workspace "${workspace.name}" deleted.`); + this.cdr.detectChanges(); + }, + error: () => { + this.isDeletingWorkspace = false; + this.deleteWorkspaceError = 'Unable to delete workspace. Please try again.'; + this.cdr.detectChanges(); } - this.workspaces = this.workspaces.filter(ws => ws.id !== workspace.id); - this.cdr.detectChanges(); }); } @@ -102,10 +139,24 @@ export class Dashboard implements OnInit { this.signalService.archive(signalId).subscribe(() => this.loadSignals()); } + isWorkspacePendingDelete(workspace: Workspace): boolean { + return this.pendingDeleteWorkspace?.id === workspace.id; + } + private loadSignals(): void { this.signalService.getSignals(this.filters).subscribe(signals => { this.signals = signals; this.cdr.detectChanges(); }); } + + private showDeleteSuccess(message: string): void { + this.deleteWorkspaceSuccess = message; + if (this.deleteNoticeTimeoutId !== undefined) { + clearTimeout(this.deleteNoticeTimeoutId); + } + this.deleteNoticeTimeoutId = setTimeout(() => { + this.deleteWorkspaceSuccess = ''; + }, 2600); + } }