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
4 changes: 4 additions & 0 deletions src/app/components/signal-board/signal-board.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
background: #111827;
}

.signal-source.jira {
background: #2563eb;
}

.signal-card h3 {
margin: 0;
font-size: 1.05rem;
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/signal-board/signal-board.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ <h3>{{ signal.title }}</h3>
<div class="signal-badges">
<span class="badge neutral" *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>
<span class="badge" *ngFor="let label of signal.metadata.labels">{{ label }}</span>
</div>

Expand Down
18 changes: 15 additions & 3 deletions src/app/components/signal-board/signal-board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,40 @@ export class SignalBoardComponent {
if (signal.sourceType === 'slack') {
return 'Slack Message';
}
if (signal.sourceType === 'jira') {
return signal.metadata.issueType || 'Jira Issue';
}

return signal.metadata.type === 'pull_request' ? 'Pull Request' : 'Issue';
}

getSourceLabel(signal: Signal): string {
return signal.sourceType === 'slack' ? 'Slack' : 'GitHub';
if (signal.sourceType === 'slack') return 'Slack';
if (signal.sourceType === 'jira') return 'Jira';
return 'GitHub';
}

getSourceClass(signal: Signal): string {
return signal.sourceType === 'slack' ? 'slack' : 'github';
if (signal.sourceType === 'slack') return 'slack';
if (signal.sourceType === 'jira') return 'jira';
return 'github';
}

getPrimaryContext(signal: Signal): string {
if (signal.sourceType === 'slack') {
return `#${this.getSlackChannel(signal)} in Slack`;
}
if (signal.sourceType === 'jira') {
return `${signal.metadata.projectKey} in Jira`;
}

return `${this.getTypeLabel(signal)} #${signal.metadata.number} in ${signal.metadata.repository}`;
}

getOpenLabel(signal: Signal): string {
return signal.sourceType === 'slack' ? 'Open in Slack' : 'Open in GitHub';
if (signal.sourceType === 'slack') return 'Open in Slack';
if (signal.sourceType === 'jira') return 'Open in Jira';
return 'Open in GitHub';
}

getSlackChannel(signal: Signal): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
color: #9d174d;
}

.provider-badge.jira-badge {
background: #e0e7ff;
color: #3730a3;
}

.card-actions,
.section-head {
display: flex;
Expand Down Expand Up @@ -110,6 +115,11 @@
background: #7c3aed;
}

.jira-btn {
border-color: #2563eb;
background: #2563eb;
}

.secondary-btn {
border: 1px solid #cbd5e1;
background: #fff;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,58 @@ <h3>GitHub sync status</h3>
<span *ngIf="githubSyncStatus.itemsSynced">({{ githubSyncStatus.itemsSynced }} items)</span>
</p>
</section>

<!-- Jira Integration -->
<article class="integration-card">
<div class="card-copy">
<span class="provider-badge jira-badge">Jira</span>
<h3>Jira connection</h3>
<p *ngIf="isJiraConnected; else jiraDisconnectedState">
Connected to Jira.
</p>
<ng-template #jiraDisconnectedState>
<p>Not connected yet. Start the OAuth flow to import assigned issues from Jira.</p>
</ng-template>
</div>

<div class="card-actions">
<button *ngIf="!isJiraConnected" type="button" class="primary-btn jira-btn" (click)="connectJira()">Connect Jira</button>
<button *ngIf="isJiraConnected" type="button" class="secondary-btn" (click)="disconnectJira()">Disconnect</button>
<button *ngIf="isJiraConnected" type="button" class="primary-btn" [disabled]="isJiraSyncing" (click)="syncJiraNow()">
{{ isJiraSyncing ? 'Syncing...' : 'Sync Now' }}
</button>
</div>
</article>

<p class="feedback success" *ngIf="jiraFeedbackMessage">{{ jiraFeedbackMessage }}</p>
<p class="feedback error" *ngIf="jiraErrorMessage">{{ jiraErrorMessage }}</p>

<section class="selection-card" *ngIf="isJiraConnected">
<div class="section-head">
<div>
<h3>Jira Cloud Resources</h3>
<p>Accessible Atlassian cloud sites.</p>
</div>
</div>

<div class="repo-list">
<div class="repo-item" *ngFor="let resource of jiraResources">
<span>
<strong>{{ resource.name }}</strong>
<small>{{ resource.url }}</small>
</span>
</div>
</div>
</section>

<section class="sync-card" *ngIf="isJiraConnected">
<h3>Jira sync status</h3>
<p *ngIf="jiraLastSyncAt">Last sync: {{ jiraLastSyncAt | date: 'medium' }}</p>
<p *ngIf="!jiraLastSyncAt">No sync has run yet.</p>
<p *ngIf="jiraSyncStatus">
Current status:
<strong>{{ jiraSyncStatus.status }}</strong>
</p>
</section>

</section>
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ describe('WorkspaceIntegrationsComponent', () => {
'connectGitHub',
'updateGitHubRepos',
'disconnectGitHub',
'syncGitHub'
'syncGitHub',
'getJiraProjects',
'connectJira',
'disconnectJira',
'syncJira'
]);

mockIntegrationService.getSlackChannels.and.returnValue(of({
Expand Down Expand Up @@ -49,6 +53,15 @@ describe('WorkspaceIntegrationsComponent', () => {
mockIntegrationService.disconnectGitHub.and.returnValue(of(void 0));
mockIntegrationService.connectGitHub.and.returnValue(of(void 0));

mockIntegrationService.getJiraProjects.and.returnValue(of({
connected: false,
resources: [],
lastSyncAt: undefined
}));
mockIntegrationService.connectJira.and.returnValue(of(void 0));
mockIntegrationService.disconnectJira.and.returnValue(of(void 0));
mockIntegrationService.syncJira.and.returnValue(of({ status: 'in_progress' }));

await TestBed.configureTestingModule({
imports: [WorkspaceIntegrationsComponent],
providers: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { IntegrationService } from '../../services/integration.service';
import { GitHubRepo, SyncStatus } from '../../models/github-integration.model';
import { SlackChannel } from '../../models/slack-integration.model';
import { AtlassianResource, JiraSyncStatus } from '../../models/jira-integration.model';

@Component({
selector: 'app-workspace-integrations',
Expand All @@ -12,7 +13,7 @@ import { SlackChannel } from '../../models/slack-integration.model';
templateUrl: './workspace-integrations.html',
styleUrl: './workspace-integrations.css'
})
export class WorkspaceIntegrationsComponent implements OnInit {
export class WorkspaceIntegrationsComponent implements OnInit, OnDestroy {
private readonly route = inject(ActivatedRoute);
private readonly integrationService = inject(IntegrationService);

Expand All @@ -39,10 +40,40 @@ export class WorkspaceIntegrationsComponent implements OnInit {
isGitHubSaving = false;
isSyncing = false;

isJiraConnected = false;
jiraResources: AtlassianResource[] = [];
jiraLastSyncAt?: Date;
jiraSyncStatus?: JiraSyncStatus;
jiraFeedbackMessage = '';
jiraErrorMessage = '';
isJiraSyncing = false;

ngOnInit(): void {
this.workspaceId = this.route.snapshot.paramMap.get('id') ?? '';
this.loadAllIntegrations();
window.addEventListener('focus', this.onWindowFocus);
}

ngOnDestroy(): void {
window.removeEventListener('focus', this.onWindowFocus);
}

private onWindowFocus = (): void => {
// If we have an active feedback message, let's refresh to see if connection succeeded in background
if (this.slackFeedbackMessage === 'Starting Slack connection...' ||
this.githubFeedbackMessage === 'Starting GitHub connection...' ||
this.jiraFeedbackMessage === 'Starting Jira connection...') {
this.slackFeedbackMessage = '';
this.githubFeedbackMessage = '';
this.jiraFeedbackMessage = '';
this.loadAllIntegrations();
}
};

private loadAllIntegrations(): void {
this.loadSlackChannels();
this.loadRepos();
this.loadJira();
}

connectSlack(): void {
Expand Down Expand Up @@ -82,6 +113,30 @@ export class WorkspaceIntegrationsComponent implements OnInit {
});
}

connectJira(): void {
this.jiraErrorMessage = '';
this.jiraFeedbackMessage = 'Starting Jira connection...';
this.integrationService.connectJira(this.workspaceId).subscribe({
error: (error: Error) => {
this.jiraErrorMessage = error.message;
this.jiraFeedbackMessage = '';
}
});
}

disconnectJira(): void {
this.integrationService.disconnectJira(this.workspaceId).subscribe({
next: () => {
this.jiraSyncStatus = undefined;
this.jiraFeedbackMessage = 'Jira integration disconnected.';
this.loadJira();
},
error: (error: Error) => {
this.jiraErrorMessage = error.message;
}
});
}

toggleSlackChannel(channelId: string, checked: boolean): void {
this.selectedSlackChannelIds = checked
? [...this.selectedSlackChannelIds, channelId]
Expand Down Expand Up @@ -155,6 +210,25 @@ export class WorkspaceIntegrationsComponent implements OnInit {
});
}

syncJiraNow(): void {
this.isJiraSyncing = true;
this.jiraErrorMessage = '';
this.integrationService.syncJira(this.workspaceId).subscribe({
next: (status) => {
this.jiraSyncStatus = status;
this.isJiraSyncing = false;
this.jiraFeedbackMessage = status.status === 'in_progress'
? 'Jira sync started. Signals will refresh shortly.'
: 'Jira sync could not be started.';
this.loadJira();
},
error: (error: Error) => {
this.isJiraSyncing = false;
this.jiraErrorMessage = error.message;
}
});
}

isSlackChannelSelected(channelId: string): boolean {
return this.selectedSlackChannelIds.includes(channelId);
}
Expand Down Expand Up @@ -184,4 +258,12 @@ export class WorkspaceIntegrationsComponent implements OnInit {
this.githubLastSyncAt = response.lastSyncAt;
});
}

private loadJira(): void {
this.integrationService.getJiraProjects(this.workspaceId).subscribe(response => {
this.isJiraConnected = response.connected;
this.jiraResources = response.resources;
this.jiraLastSyncAt = response.lastSyncAt;
});
}
}
12 changes: 11 additions & 1 deletion src/app/components/workspace/create-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@ export class CreateWorkspace {
this.error = '';

this.workspaceService.createWorkspace(trimmedName, this.description.trim()).subscribe({
next: () => this.router.navigate(['/dashboard']),
next: () => {
this.router.navigate(['/dashboard']).then(success => {
if (!success) {
this.isSubmitting = false;
this.error = 'Navigation failed. Workspace was created, but unable to load dashboard.';
}
}).catch(() => {
this.isSubmitting = false;
this.error = 'An error occurred while loading the dashboard.';
});
},
error: () => {
this.error = 'Unable to create workspace. Please try again.';
this.isSubmitting = false;
Expand Down
26 changes: 26 additions & 0 deletions src/app/models/jira-integration.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface JiraProject {
id: string;
key: string;
name: string;
avatarUrl: string;
}

export interface AtlassianResource {
id: string;
url: string;
name: string;
scopes: string[];
avatarUrl: string;
}

export interface JiraSyncStatus {
status: 'idle' | 'in_progress' | 'completed' | 'error';
lastSyncAt?: string;
message?: string;
}

export interface JiraIntegrationResponse {
connected: boolean;
resources: AtlassianResource[];
lastSyncAt?: string;
}
7 changes: 5 additions & 2 deletions src/app/models/signal.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type SignalSourceType = 'slack' | 'github' | 'email' | 'decision';
export type SignalSourceType = 'slack' | 'github' | 'email' | 'decision' | 'jira';
export type SignalStatus = 'unread' | 'read' | 'archived';

export interface SignalMetadata {
Expand All @@ -10,6 +10,9 @@ export interface SignalMetadata {
assignees?: string[];
createdAt?: Date;
updatedAt?: Date;
issueType?: string;
priority?: string;
projectKey?: string;
[key: string]: unknown;
}

Expand All @@ -28,6 +31,6 @@ export interface Signal {
}

export interface SignalFilters {
source: 'all' | 'github' | 'slack' | 'decision';
source: 'all' | 'github' | 'slack' | 'decision' | 'jira';
status: 'all' | SignalStatus;
}
Loading
Loading