diff --git a/src/app/components/signal-board/signal-board.css b/src/app/components/signal-board/signal-board.css index b0d2bd2..9441de0 100644 --- a/src/app/components/signal-board/signal-board.css +++ b/src/app/components/signal-board/signal-board.css @@ -167,14 +167,16 @@ pointer-events: none; } -.jira-comment-section { +.jira-comment-section, +.github-comment-section { display: flex; gap: 0.5rem; width: 100%; margin-bottom: 0.75rem; } -.jira-comment-section input { +.jira-comment-section input, +.github-comment-section input { flex: 1; border: 1px solid #cbd5e1; border-radius: 999px; @@ -184,7 +186,8 @@ transition: border-color 0.2s; } -.jira-comment-section input:focus { +.jira-comment-section input:focus, +.github-comment-section input:focus { border-color: #6366f1; } @@ -194,6 +197,17 @@ flex-wrap: wrap; } +/* Badge variants */ +.badge.success { + background: #dcfce7; + color: #15803d; +} + +.badge.danger { + background: #fee2e2; + color: #b91c1c; +} + /* Toast */ .toast-container { position: fixed; diff --git a/src/app/components/signal-board/signal-board.html b/src/app/components/signal-board/signal-board.html index 37fff06..c041f58 100644 --- a/src/app/components/signal-board/signal-board.html +++ b/src/app/components/signal-board/signal-board.html @@ -26,7 +26,11 @@

{{ signal.title }}

{{ signal.content }}

- {{ signal.metadata.state }} + + {{ signal.metadata.state }} + #{{ getSlackChannel(signal) }} {{ signal.metadata.issueType }} {{ signal.metadata.priority }} @@ -46,8 +50,16 @@

{{ signal.title }}

+
+ + +
+
{{ getOpenLabel(signal) }} +
diff --git a/src/app/components/signal-board/signal-board.ts b/src/app/components/signal-board/signal-board.ts index 1dfa39d..7dc7441 100644 --- a/src/app/components/signal-board/signal-board.ts +++ b/src/app/components/signal-board/signal-board.ts @@ -23,6 +23,8 @@ export class SignalBoardComponent { loadingTransitions: { [signalId: string]: boolean } = {}; jiraComments: { [signalId: string]: string } = {}; submittingComment: { [signalId: string]: boolean } = {}; + githubComments: { [signalId: string]: string } = {}; + submittingGitHubComment: { [signalId: string]: boolean } = {}; toastMessage = ''; trackBySignal(_: number, signal: Signal): string { @@ -148,4 +150,52 @@ export class SignalBoardComponent { this.cdr.detectChanges(); }, 4000); } + + addGitHubComment(signal: Signal) { + const workspaceId = String(signal.workspaceId ?? ''); + const repo = signal.metadata.repository; + const number = signal.metadata.number; + const comment = this.githubComments[signal.id]; + + if (!comment || !comment.trim() || !repo || !number) return; + + this.submittingGitHubComment[signal.id] = true; + this.integrationService.addGitHubComment(workspaceId, repo, number, comment).subscribe({ + next: () => { + this.showToast('Comment added to GitHub'); + this.githubComments[signal.id] = ''; + this.submittingGitHubComment[signal.id] = false; + this.cdr.detectChanges(); + }, + error: () => { + this.showToast('Failed to add GitHub comment'); + this.submittingGitHubComment[signal.id] = false; + this.cdr.detectChanges(); + } + }); + } + + toggleGitHubState(signal: Signal) { + const workspaceId = String(signal.workspaceId ?? ''); + const repo = signal.metadata.repository; + const number = signal.metadata.number; + const currentState = signal.metadata.state; + + if (!repo || !number) return; + + const newState = currentState === 'open' ? 'closed' : 'open'; + + this.integrationService.updateGitHubIssueState(workspaceId, repo, number, newState).subscribe({ + next: () => { + this.showToast(`Issue ${newState === 'closed' ? 'closed' : 'reopened'}`); + if (signal.metadata) { + signal.metadata.state = newState; + } + this.cdr.detectChanges(); + }, + error: () => { + this.showToast('Failed to update GitHub status'); + } + }); + } } diff --git a/src/app/components/workspace-integrations/workspace-integrations.ts b/src/app/components/workspace-integrations/workspace-integrations.ts index 5c2b98d..1cd37ab 100644 --- a/src/app/components/workspace-integrations/workspace-integrations.ts +++ b/src/app/components/workspace-integrations/workspace-integrations.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, OnDestroy, inject } from '@angular/core'; -import { ActivatedRoute, RouterLink } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { IntegrationService } from '../../services/integration.service'; import { GitHubRepo, SyncStatus } from '../../models/github-integration.model'; import { SlackChannel } from '../../models/slack-integration.model'; @@ -9,7 +9,7 @@ import { AtlassianResource, JiraSyncStatus } from '../../models/jira-integration @Component({ selector: 'app-workspace-integrations', standalone: true, - imports: [CommonModule, RouterLink], + imports: [CommonModule], templateUrl: './workspace-integrations.html', styleUrl: './workspace-integrations.css' }) @@ -108,7 +108,7 @@ export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy { connectGitHub(): void { this.githubErrorMessage = ''; this.githubFeedbackMessage = 'Starting GitHub connection...'; - this.integrationService.connectGitHub().subscribe({ + this.integrationService.connectGitHub(this.workspaceId).subscribe({ error: (error: Error) => { this.githubErrorMessage = error.message; this.githubFeedbackMessage = ''; @@ -117,7 +117,7 @@ export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy { } disconnectGitHub(): void { - this.integrationService.disconnectGitHub().subscribe(() => { + this.integrationService.disconnectGitHub(this.workspaceId).subscribe(() => { this.githubSyncStatus = undefined; this.githubFeedbackMessage = 'GitHub integration disconnected.'; this.loadRepos(); @@ -189,7 +189,7 @@ export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy { saveRepoSelection(): void { this.isGitHubSaving = true; this.githubErrorMessage = ''; - this.integrationService.updateGitHubRepos(this.selectedRepoIds).subscribe({ + this.integrationService.updateGitHubRepos(this.workspaceId, this.selectedRepoIds).subscribe({ next: () => { this.isGitHubSaving = false; this.githubFeedbackMessage = 'Repository selection saved.'; @@ -205,7 +205,7 @@ export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy { syncNow(): void { this.isSyncing = true; this.githubErrorMessage = ''; - this.integrationService.syncGitHub().subscribe({ + this.integrationService.syncGitHub(this.workspaceId).subscribe({ next: (status) => { this.githubSyncStatus = status; this.isSyncing = false; @@ -260,7 +260,7 @@ export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy { } private loadRepos(): void { - this.integrationService.getGitHubRepos().subscribe(response => { + this.integrationService.getGitHubRepos(this.workspaceId).subscribe(response => { this.isGitHubConnected = response.connected; this.repos = response.repos; this.selectedRepoIds = response.repos.filter(repo => repo.isConnected).map(repo => repo.id); diff --git a/src/app/components/workspace-members/workspace-members.ts b/src/app/components/workspace-members/workspace-members.ts index 9d758ba..fb1ec57 100644 --- a/src/app/components/workspace-members/workspace-members.ts +++ b/src/app/components/workspace-members/workspace-members.ts @@ -1,14 +1,14 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute, RouterLink } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { Invitation, InvitationRole, WorkspaceMember, WorkspaceRole } from '../../models/workspace-member.model'; import { WorkspaceMemberService } from '../../services/workspace-member.service'; @Component({ selector: 'app-workspace-members', standalone: true, - imports: [CommonModule, FormsModule, RouterLink], + imports: [CommonModule, FormsModule], templateUrl: './workspace-members.html', styleUrl: './workspace-members.css' }) diff --git a/src/app/services/integration.service.ts b/src/app/services/integration.service.ts index 17cbd8a..923ce6d 100644 --- a/src/app/services/integration.service.ts +++ b/src/app/services/integration.service.ts @@ -114,23 +114,24 @@ export class IntegrationService { ); } - getGitHubAuthUrl(): Observable<{ authUrl: string }> { - return this.http.get(`${this.apiUrl}/github/auth`).pipe( + getGitHubAuthUrl(workspaceId: string): Observable<{ authUrl: string }> { + const params = new HttpParams().set('workspace_id', workspaceId); + return this.http.get(`${this.apiUrl}/github/auth`, { params }).pipe( map((response) => ({ authUrl: response.auth_url })), catchError((error) => throwError(() => toError(error, 'Unable to start GitHub connection.'))), ); } - connectGitHub(): Observable { - return this.getGitHubAuthUrl().pipe( + connectGitHub(workspaceId: string): Observable { + return this.getGitHubAuthUrl(workspaceId).pipe( map(({ authUrl }) => { window.open(authUrl, '_blank'); }), ); } - getGitHubRepos(): Observable { - return this.getIntegrations().pipe( + getGitHubRepos(workspaceId: string): Observable { + return this.getIntegrations(workspaceId).pipe( switchMap((integrations) => { const githubIntegration = integrations.find((integration) => integration.provider === 'github'); if (!githubIntegration) { @@ -142,8 +143,9 @@ export class IntegrationService { const metadata = this.parseMetadata(githubIntegration.metadata); const selectedRepoIds = this.readNumberArray(metadata['selected_repo_ids']); + const params = new HttpParams().set('workspace_id', workspaceId); - return this.http.get(`${this.apiUrl}/github/repos`).pipe( + return this.http.get(`${this.apiUrl}/github/repos`, { params }).pipe( map((repos) => ({ connected: true, repos: repos.map((repo) => this.mapGitHubRepo(repo, selectedRepoIds)), @@ -157,20 +159,23 @@ export class IntegrationService { ); } - updateGitHubRepos(repoIds: number[]): Observable { - return this.http.patch(`${this.apiUrl}/github/repos`, { repo_ids: repoIds }).pipe( + updateGitHubRepos(workspaceId: string, repoIds: number[]): Observable { + const params = new HttpParams().set('workspace_id', workspaceId); + return this.http.patch(`${this.apiUrl}/github/repos`, { repo_ids: repoIds }, { params }).pipe( catchError((error) => throwError(() => toError(error, 'Unable to save repository selection.'))), ); } - disconnectGitHub(): Observable { - return this.http.delete(`${this.apiUrl}/github`).pipe( + disconnectGitHub(workspaceId: string): Observable { + const params = new HttpParams().set('workspace_id', workspaceId); + return this.http.delete(`${this.apiUrl}/github`, { params }).pipe( catchError((error) => throwError(() => toError(error, 'Unable to disconnect GitHub.'))), ); } - syncGitHub(): Observable { - return this.http.post(`${this.apiUrl}/github/sync`, {}).pipe( + syncGitHub(workspaceId: string): Observable { + const params = new HttpParams().set('workspace_id', workspaceId); + return this.http.post(`${this.apiUrl}/github/sync`, {}, { params }).pipe( map((response) => { const status: SyncStatus['status'] = response.status === 'sync_started' ? 'in_progress' : 'failed'; return { @@ -182,6 +187,20 @@ export class IntegrationService { ); } + addGitHubComment(workspaceId: string, repo: string, number: number, comment: string): Observable { + const params = new HttpParams().set('workspace_id', workspaceId); + return this.http.post(`${this.apiUrl}/github/issues/${number}/comments`, { repo, body: comment }, { params }).pipe( + catchError((error) => throwError(() => toError(error, 'Unable to add GitHub comment.'))), + ); + } + + updateGitHubIssueState(workspaceId: string, repo: string, number: number, state: string): Observable { + const params = new HttpParams().set('workspace_id', workspaceId); + return this.http.patch(`${this.apiUrl}/github/issues/${number}/state`, { repo, state }, { params }).pipe( + catchError((error) => throwError(() => toError(error, 'Unable to update GitHub issue status.'))), + ); + } + getJiraAuthUrl(workspaceId: string): Observable<{ authUrl: string }> { const params = new HttpParams().set('workspace_id', workspaceId); return this.http.get(`${this.apiUrl}/jira/auth`, { params }).pipe(