- Implemented unit tests for the Package model, covering methods such as generateId, findById, findByName, and version management. - Created unit tests for the Repository model, including repository creation, name validation, and retrieval methods. - Added tests for the Session model, focusing on session creation, validation, and invalidation. - Developed unit tests for the User model, ensuring user creation, password hashing, and retrieval methods function correctly. - Implemented AuthService tests, validating login, token refresh, and session management. - Added TokenService tests, covering token creation, validation, and revocation processes.
161 lines
6.7 KiB
TypeScript
161 lines
6.7 KiB
TypeScript
import { Component, inject, signal, OnInit } from '@angular/core';
|
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
import { ApiService, type IOrganization, type IRepository } from '../../core/services/api.service';
|
|
import { ToastService } from '../../core/services/toast.service';
|
|
|
|
@Component({
|
|
selector: 'app-organization-detail',
|
|
standalone: true,
|
|
imports: [RouterLink],
|
|
template: `
|
|
<div class="p-6 max-w-7xl mx-auto">
|
|
@if (loading()) {
|
|
<div class="flex items-center justify-center py-12">
|
|
<svg class="animate-spin h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
</svg>
|
|
</div>
|
|
} @else if (organization()) {
|
|
<!-- Header -->
|
|
<div class="flex items-start justify-between mb-8">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-16 h-16 bg-muted flex items-center justify-center">
|
|
<span class="font-mono text-2xl font-medium text-muted-foreground">
|
|
{{ organization()!.name.charAt(0).toUpperCase() }}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<h1 class="font-mono text-2xl font-bold text-foreground">{{ organization()!.displayName }}</h1>
|
|
<p class="font-mono text-muted-foreground">@{{ organization()!.name }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if (organization()!.isPublic) {
|
|
<span class="badge-accent">Public</span>
|
|
} @else {
|
|
<span class="badge-primary">Private</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (organization()!.description) {
|
|
<p class="font-mono text-muted-foreground mb-8">{{ organization()!.description }}</p>
|
|
}
|
|
|
|
<!-- Repositories Section -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="section-header">
|
|
<div class="section-indicator"></div>
|
|
<span class="section-label">Repositories</span>
|
|
</div>
|
|
<button class="btn-primary btn-sm">
|
|
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
New Repository
|
|
</button>
|
|
</div>
|
|
|
|
@if (repositories().length === 0) {
|
|
<div class="card card-content text-center py-8">
|
|
<p class="font-mono text-muted-foreground">No repositories yet</p>
|
|
</div>
|
|
} @else {
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
@for (repo of repositories(); track repo.id) {
|
|
<a [routerLink]="['repositories', repo.id]" class="card card-content hover:border-primary/50 transition-colors">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<h3 class="font-mono font-medium text-foreground">{{ repo.displayName }}</h3>
|
|
<p class="font-mono text-sm text-muted-foreground">{{ repo.name }}</p>
|
|
</div>
|
|
@if (repo.isPublic) {
|
|
<span class="badge-accent">Public</span>
|
|
}
|
|
</div>
|
|
@if (repo.description) {
|
|
<p class="font-mono text-sm text-muted-foreground mt-2 line-clamp-2">{{ repo.description }}</p>
|
|
}
|
|
<div class="mt-3 flex items-center gap-4">
|
|
<div class="flex items-center gap-1 font-mono text-sm text-muted-foreground">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
</svg>
|
|
{{ repo.packageCount }} packages
|
|
</div>
|
|
<div class="flex gap-1">
|
|
@for (protocol of repo.protocols; track protocol) {
|
|
<span class="badge-primary text-xs">{{ protocol }}</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="mb-4">
|
|
<div class="section-header">
|
|
<div class="section-indicator"></div>
|
|
<span class="section-label">Statistics</span>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div class="card card-content">
|
|
<p class="font-mono text-sm text-muted-foreground">Members</p>
|
|
<p class="font-mono text-2xl font-bold text-foreground">{{ organization()!.memberCount }}</p>
|
|
</div>
|
|
<div class="card card-content">
|
|
<p class="font-mono text-sm text-muted-foreground">Repositories</p>
|
|
<p class="font-mono text-2xl font-bold text-foreground">{{ repositories().length }}</p>
|
|
</div>
|
|
<div class="card card-content">
|
|
<p class="font-mono text-sm text-muted-foreground">Created</p>
|
|
<p class="font-mono text-2xl font-bold text-foreground">{{ formatDate(organization()!.createdAt) }}</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
`,
|
|
})
|
|
export class OrganizationDetailComponent implements OnInit {
|
|
private route = inject(ActivatedRoute);
|
|
private apiService = inject(ApiService);
|
|
private toastService = inject(ToastService);
|
|
|
|
organization = signal<IOrganization | null>(null);
|
|
repositories = signal<IRepository[]>([]);
|
|
loading = signal(true);
|
|
|
|
ngOnInit(): void {
|
|
const orgName = this.route.snapshot.paramMap.get('orgName');
|
|
if (orgName) {
|
|
this.loadData(orgName);
|
|
}
|
|
}
|
|
|
|
private async loadData(orgName: string): Promise<void> {
|
|
this.loading.set(true);
|
|
try {
|
|
const [org, reposResponse] = await Promise.all([
|
|
this.apiService.getOrganization(orgName).toPromise(),
|
|
this.apiService.getRepositories(orgName).toPromise(),
|
|
]);
|
|
this.organization.set(org || null);
|
|
this.repositories.set(reposResponse?.repositories || []);
|
|
} catch (error) {
|
|
this.toastService.error('Failed to load organization');
|
|
} finally {
|
|
this.loading.set(false);
|
|
}
|
|
}
|
|
|
|
formatDate(dateStr: string): string {
|
|
return new Date(dateStr).toLocaleDateString();
|
|
}
|
|
}
|