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
20 changes: 17 additions & 3 deletions src/app/components/signal-board/signal-board.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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;
Expand Down
14 changes: 13 additions & 1 deletion src/app/components/signal-board/signal-board.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ <h3>{{ signal.title }}</h3>
<p class="signal-body">{{ signal.content }}</p>

<div class="signal-badges">
<span class="badge neutral" *ngIf="signal.sourceType === 'github'">{{ signal.metadata.state }}</span>
<span class="badge"
[ngClass]="signal.metadata.state === 'open' ? 'success' : 'danger'"
*ngIf="signal.sourceType === 'github'">
{{ signal.metadata.state }}
</span>
<span class="badge neutral" *ngIf="signal.sourceType === 'slack'">#{{ getSlackChannel(signal) }}</span>
<span class="badge neutral" *ngIf="signal.sourceType === 'jira'">{{ signal.metadata.issueType }}</span>
<span class="badge neutral" *ngIf="signal.sourceType === 'jira' && signal.metadata.priority">{{ signal.metadata.priority }}</span>
Expand All @@ -46,8 +50,16 @@ <h3>{{ signal.title }}</h3>
<button type="button" class="ghost-btn" [disabled]="submittingComment[signal.id] || !jiraComments[signal.id]" (click)="addJiraComment(signal)">Comment</button>
</div>

<div class="github-comment-section" *ngIf="signal.sourceType === 'github'">
<input type="text" placeholder="Reply on GitHub..." [(ngModel)]="githubComments[signal.id]" [disabled]="submittingGitHubComment[signal.id]">
<button type="button" class="ghost-btn" [disabled]="submittingGitHubComment[signal.id] || !githubComments[signal.id]" (click)="addGitHubComment(signal)">Comment</button>
</div>

<div class="action-buttons-group">
<a class="link-btn" *ngIf="signal.url" [href]="signal.url" target="_blank" rel="noreferrer">{{ getOpenLabel(signal) }}</a>
<button type="button" class="ghost-btn" *ngIf="signal.sourceType === 'github'" (click)="toggleGitHubState(signal)">
{{ signal.metadata.state === 'open' ? 'Close Issue' : 'Reopen' }}
</button>
<button type="button" class="ghost-btn" (click)="markAsRead.emit(signal.id)">Mark as read</button>
<button type="button" class="ghost-btn" (click)="archive.emit(signal.id)">Archive</button>
</div>
Expand Down
50 changes: 50 additions & 0 deletions src/app/components/signal-board/signal-board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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');
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'
})
Expand Down Expand Up @@ -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 = '';
Expand All @@ -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();
Expand Down Expand Up @@ -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.';
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/workspace-members/workspace-members.ts
Original file line number Diff line number Diff line change
@@ -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'
})
Expand Down
45 changes: 32 additions & 13 deletions src/app/services/integration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,24 @@ export class IntegrationService {
);
}

getGitHubAuthUrl(): Observable<{ authUrl: string }> {
return this.http.get<OAuthResponse>(`${this.apiUrl}/github/auth`).pipe(
getGitHubAuthUrl(workspaceId: string): Observable<{ authUrl: string }> {
const params = new HttpParams().set('workspace_id', workspaceId);
return this.http.get<OAuthResponse>(`${this.apiUrl}/github/auth`, { params }).pipe(
map((response) => ({ authUrl: response.auth_url })),
catchError((error) => throwError(() => toError(error, 'Unable to start GitHub connection.'))),
);
}

connectGitHub(): Observable<void> {
return this.getGitHubAuthUrl().pipe(
connectGitHub(workspaceId: string): Observable<void> {
return this.getGitHubAuthUrl(workspaceId).pipe(
map(({ authUrl }) => {
window.open(authUrl, '_blank');
}),
);
}

getGitHubRepos(): Observable<GitHubConnectionState> {
return this.getIntegrations().pipe(
getGitHubRepos(workspaceId: string): Observable<GitHubConnectionState> {
return this.getIntegrations(workspaceId).pipe(
switchMap((integrations) => {
const githubIntegration = integrations.find((integration) => integration.provider === 'github');
if (!githubIntegration) {
Expand All @@ -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<GitHubRepoResponse[]>(`${this.apiUrl}/github/repos`).pipe(
return this.http.get<GitHubRepoResponse[]>(`${this.apiUrl}/github/repos`, { params }).pipe(
map((repos) => ({
connected: true,
repos: repos.map((repo) => this.mapGitHubRepo(repo, selectedRepoIds)),
Expand All @@ -157,20 +159,23 @@ export class IntegrationService {
);
}

updateGitHubRepos(repoIds: number[]): Observable<void> {
return this.http.patch<void>(`${this.apiUrl}/github/repos`, { repo_ids: repoIds }).pipe(
updateGitHubRepos(workspaceId: string, repoIds: number[]): Observable<void> {
const params = new HttpParams().set('workspace_id', workspaceId);
return this.http.patch<void>(`${this.apiUrl}/github/repos`, { repo_ids: repoIds }, { params }).pipe(
catchError((error) => throwError(() => toError(error, 'Unable to save repository selection.'))),
);
}

disconnectGitHub(): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/github`).pipe(
disconnectGitHub(workspaceId: string): Observable<void> {
const params = new HttpParams().set('workspace_id', workspaceId);
return this.http.delete<void>(`${this.apiUrl}/github`, { params }).pipe(
catchError((error) => throwError(() => toError(error, 'Unable to disconnect GitHub.'))),
);
}

syncGitHub(): Observable<SyncStatus> {
return this.http.post<GitHubSyncResponse>(`${this.apiUrl}/github/sync`, {}).pipe(
syncGitHub(workspaceId: string): Observable<SyncStatus> {
const params = new HttpParams().set('workspace_id', workspaceId);
return this.http.post<GitHubSyncResponse>(`${this.apiUrl}/github/sync`, {}, { params }).pipe(
map((response) => {
const status: SyncStatus['status'] = response.status === 'sync_started' ? 'in_progress' : 'failed';
return {
Expand All @@ -182,6 +187,20 @@ export class IntegrationService {
);
}

addGitHubComment(workspaceId: string, repo: string, number: number, comment: string): Observable<void> {
const params = new HttpParams().set('workspace_id', workspaceId);
return this.http.post<void>(`${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<void> {
const params = new HttpParams().set('workspace_id', workspaceId);
return this.http.patch<void>(`${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<OAuthResponse>(`${this.apiUrl}/jira/auth`, { params }).pipe(
Expand Down
Loading