Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions src/app/components/dashboard/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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;
Expand Down
39 changes: 34 additions & 5 deletions src/app/components/dashboard/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<div class="hero-copy">
<h1>Decision Workspace Dashboard</h1>
<p>Track active workspaces, Slack conversations, and GitHub work in one place with clean structure and minimal noise.</p>
<p>Track active workspaces, Slack conversations, GitHub updates, and JIRA work in one place with clean structure and minimal noise.</p>
</div>
</section>

Expand All @@ -32,6 +32,8 @@ <h1>Decision Workspace Dashboard</h1>
<h2>Your Workspaces</h2>
<a routerLink="/workspace/create" class="create-btn">Create Workspace</a>
</div>
<p *ngIf="deleteWorkspaceSuccess" class="workspace-feedback success">{{ deleteWorkspaceSuccess }}</p>
<p *ngIf="deleteWorkspaceError && pendingDeleteWorkspace === null" class="workspace-feedback error">{{ deleteWorkspaceError }}</p>

<div class="workspace-list" *ngIf="workspaces.length > 0; else empty">
<article *ngFor="let ws of workspaces" class="workspace-card">
Expand All @@ -40,8 +42,15 @@ <h3>{{ ws.name }}</h3>
<small>Created: {{ ws.createdDate | date: 'mediumDate' }}</small>
<div class="card-actions">
<a [routerLink]="['/workspaces', ws.id, 'decisions']" class="action-btn open-btn">Open</a>
<a *ngIf="canEditWorkspace(ws)" [routerLink]="['/workspaces', ws.id, 'edit']" class="action-btn edit-btn">Edit</a>
<button type="button" class="action-btn delete-btn" (click)="deleteWorkspace(ws)">Delete</button>
<a [routerLink]="['/workspaces', ws.id, 'edit']" class="action-btn edit-btn">Edit</a>
<button
type="button"
class="action-btn delete-btn"
(click)="requestDeleteWorkspace(ws)"
[disabled]="isDeletingWorkspace && isWorkspacePendingDelete(ws)"
>
{{ isDeletingWorkspace && isWorkspacePendingDelete(ws) ? 'Deleting...' : 'Delete' }}
</button>
</div>
</article>
</div>
Expand All @@ -54,6 +63,25 @@ <h3>No workspaces yet</h3>
</ng-template>
</section>

<div class="modal-backdrop" *ngIf="pendingDeleteWorkspace as workspace">
<section class="delete-modal" role="dialog" aria-modal="true" aria-labelledby="delete-workspace-title">
<p class="delete-modal-eyebrow">Delete Workspace</p>
<h3 id="delete-workspace-title">Delete "{{ workspace.name }}"?</h3>
<p class="delete-modal-copy">
This will remove the workspace and related data for your account. This action cannot be undone.
</p>

<p *ngIf="deleteWorkspaceError" class="delete-modal-error">{{ deleteWorkspaceError }}</p>

<div class="delete-modal-actions">
<button type="button" class="modal-btn cancel" (click)="cancelDeleteWorkspace()" [disabled]="isDeletingWorkspace">Cancel</button>
<button type="button" class="modal-btn confirm" (click)="confirmDeleteWorkspace()" [disabled]="isDeletingWorkspace">
{{ isDeletingWorkspace ? 'Deleting...' : 'Yes, Delete' }}
</button>
</div>
</section>
</div>

<section class="content signals-section">
<div class="section-header signals-header">
<div class="title-group">
Expand All @@ -64,11 +92,12 @@ <h2>Signals</h2>
<button type="button" class="filter-pill" [class.active]="filters.source === 'all'" (click)="setSourceFilter('all')">All</button>
<button type="button" class="filter-pill" [class.active]="filters.source === 'slack'" (click)="setSourceFilter('slack')">Slack</button>
<button type="button" class="filter-pill" [class.active]="filters.source === 'github'" (click)="setSourceFilter('github')">GitHub</button>
<button type="button" class="filter-pill" [class.active]="filters.source === 'jira'" (click)="setSourceFilter('jira')">JIRA</button>
<button type="button" class="filter-pill" [class.active]="filters.status === 'all'" (click)="setStatusFilter('all')">All statuses</button>
<button type="button" class="filter-pill" [class.active]="filters.status === 'unread'" (click)="setStatusFilter('unread')">Unread</button>
</div>
</div>
<p class="section-copy">Slack messages and GitHub updates appear here as action-ready signals.</p>
<p class="section-copy">Slack messages, GitHub updates, and JIRA work appear here as action-ready signals.</p>

<app-signal-board
*ngIf="signals.length; else noSignals"
Expand All @@ -80,7 +109,7 @@ <h2>Signals</h2>
<ng-template #noSignals>
<div class="empty-state">
<h3>No matching signals</h3>
<p>Connect Slack or GitHub in workspace integrations to start pulling messages and assigned work into Sentinent.</p>
<p>Connect Slack, GitHub, or JIRA in workspace integrations to start pulling messages and assigned work into Sentinent.</p>
</div>
</ng-template>
</section>
Expand Down
32 changes: 29 additions & 3 deletions src/app/components/dashboard/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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' }
Expand All @@ -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', () => {
Expand All @@ -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();
});
});
81 changes: 66 additions & 15 deletions src/app/components/dashboard/dashboard.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -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<typeof setTimeout>;

ngOnInit() {
this.currentUserId = this.authService.getCurrentUserId();

this.workspaceService.getWorkspaces().subscribe({
next: ws => {
this.workspaces = ws;
Expand All @@ -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();
});
}

Expand All @@ -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);
}
}
Loading