Skip to content

Commit

Permalink
Improve repository popover
Browse files Browse the repository at this point in the history
  • Loading branch information
GODrums committed Nov 10, 2024
1 parent eaf08f2 commit 0197821
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 41 deletions.
51 changes: 51 additions & 0 deletions server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/TeamInfo"
delete:
tags:
- admin
operationId: removeRepositoryFromTeam
parameters:
- name: teamId
in: path
required: true
schema:
type: integer
format: int64
- name: repositoryOwner
in: path
required: true
schema:
type: string
- name: repositoryName
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/TeamInfo"
/admin/teams/{teamId}/label/{label}:
post:
tags:
Expand All @@ -108,6 +136,29 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/TeamInfo"
delete:
tags:
- admin
operationId: removeLabelFromTeam
parameters:
- name: teamId
in: path
required: true
schema:
type: integer
format: int64
- name: label
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/TeamInfo"
/admin/config/repositories:
post:
tags:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,27 @@ public ResponseEntity<TeamInfoDTO> addRepositoryToTeam(@PathVariable Long teamId
.orElseGet(() -> ResponseEntity.notFound().build());
}

@DeleteMapping("/teams/{teamId}/repository/{repositoryOwner}/{repositoryName}")
public ResponseEntity<TeamInfoDTO> removeRepositoryFromTeam(@PathVariable Long teamId, @PathVariable String repositoryOwner, @PathVariable String repositoryName) {
return adminService.removeRepositoryFromTeam(teamId, repositoryOwner + '/' + repositoryName)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

@PostMapping("/teams/{teamId}/label/{label}")
public ResponseEntity<TeamInfoDTO> addLabelToTeam(@PathVariable Long teamId, @PathVariable String label) {
return adminService.addLabelToTeam(teamId, label)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

@DeleteMapping("/teams/{teamId}/label/{label}")
public ResponseEntity<TeamInfoDTO> removeLabelFromTeam(@PathVariable Long teamId, @PathVariable String label) {
return adminService.removeLabelFromTeam(teamId, label)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

@DeleteMapping("/teams/{teamId}")
public ResponseEntity<TeamInfoDTO> deleteTeam(@PathVariable Long teamId) {
return adminService.deleteTeam(teamId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ public Optional<TeamInfoDTO> addRepositoryToTeam(Long teamId, String repositoryN
return Optional.of(TeamInfoDTO.fromTeam(team));
}

public Optional<TeamInfoDTO> removeRepositoryFromTeam(Long teamId, String repositoryName) {
logger.info("Removing repository with name: " + repositoryName + " from team with ID: " + teamId);
Optional<Team> optionalTeam = teamService.getTeam(teamId);
if (optionalTeam.isEmpty()) {
return Optional.empty();
}
Team team = optionalTeam.get();
Repository repository = repositoryRepository.findByNameWithOwner(repositoryName);
if (repository == null) {
return Optional.empty();
}
team.removeRepository(repository);
teamService.saveTeam(team);
return Optional.of(TeamInfoDTO.fromTeam(team));
}

public Optional<TeamInfoDTO> addLabelToTeam(Long teamId, String label) {
logger.info("Adding label '" + label + "' to team with ID: " + teamId);
Optional<Team> optionalTeam = teamService.getTeam(teamId);
Expand All @@ -165,6 +181,22 @@ public Optional<TeamInfoDTO> addLabelToTeam(Long teamId, String label) {
return Optional.of(TeamInfoDTO.fromTeam(team));
}

public Optional<TeamInfoDTO> removeLabelFromTeam(Long teamId, String label) {
logger.info("Removing label '" + label + "' from team with ID: " + teamId);
Optional<Team> optionalTeam = teamService.getTeam(teamId);
if (optionalTeam.isEmpty()) {
return Optional.empty();
}
Team team = optionalTeam.get();
Optional<Label> labelEntity = labelRepository.findByName(label);
if (labelEntity.isEmpty()) {
return Optional.empty();
}
team.removeLabel(labelEntity.get());
teamService.saveTeam(team);
return Optional.of(TeamInfoDTO.fromTeam(team));
}

public Optional<TeamInfoDTO> deleteTeam(Long teamId) {
logger.info("Deleting team with ID: " + teamId);
Optional<Team> optionalTeam = teamService.getTeam(teamId);
Expand Down
44 changes: 30 additions & 14 deletions webapp/src/app/admin/teams/table/teams-table.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
</brn-column-def>
<brn-column-def name="color" class="w-28">
<hlm-th *brnHeaderDef>Color</hlm-th>
<hlm-td class="font-medium tabular-nums flex gap-4" *brnCellDef="let element">
<hlm-td class="font-medium tabular-nums flex gap-2" *brnCellDef="let element">
@if (this.isLoading()) {
<hlm-skeleton class="w-4 h-4 rounded-full" [style.backgroundColor]="'#69feff'" />
<hlm-skeleton class="h-6 w-12" />
Expand All @@ -53,23 +53,36 @@
@if (this.isLoading()) {
<hlm-skeleton class="h-6 w-20" />
} @else {
@for(repo of element.repositories; track repo.id) {
<span>{{ repo.nameWithOwner }},</span>
}
<div class="flex flex-wrap items-center gap-2">
@for(repo of element.repositories; track repo.id) {

Check failure on line 57 in webapp/src/app/admin/teams/table/teams-table.component.html

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Insert `·`
<span class="text-muted-foreground border px-2 py-0.5 rounded-lg text-sm">{{ repo.nameWithOwner }}</span>
}
</div>
<brn-popover sideOffset="5" closeDelay="100">
<button id="add-repository" size="icon" variant="outline" class="h-6 w-6 p-0.5" brnPopoverTrigger hlmBtn>
<hlm-icon class="w-4 h-4" name="lucidePlus" />
</button>
<div hlmPopoverContent class="w-96 p-4 gap-4 flex flex-col items-center" *brnPopoverContent="let ctx">
<div class="flex gap-2 items-center">
<input hlmInput placeholder="Owner" [formControl]="_newRepositoryOwner" class="w-32" />
<span class="text-muted-foreground">/</span>
<input hlmInput placeholder="Repository" [formControl]="_newRepositoryName" class="w-32" />
<div hlmPopoverContent class="w-80 h-96 space-y-4" *brnPopoverContent="let ctx">
<div class="space-y-2">
<h4 class="font-medium leading-none">Repositories</h4>
<p class="text-sm text-muted-foreground">Select repositories the team actively uses.</p>
</div>
<button hlmBtn variant="default" class="space-x-2" (click)="addRepository(element)">
<span>Add</span>
<hlm-icon class="w-4 h-4" name="lucidePlus" />
</button>
<hlm-scroll-area class="border h-72 rounded-md border-border">
@for(repo of allRepositories(); track repo) {

Check failure on line 71 in webapp/src/app/admin/teams/table/teams-table.component.html

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Insert `·`
@let isSelected = isRepositoryInTeam(element, repo);
<button

Check failure on line 73 in webapp/src/app/admin/teams/table/teams-table.component.html

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Replace `⏎··················hlmBtn⏎··················variant="outline"⏎··················class="flex·items-center·justify-between·w-full"⏎··················(click)="toggleRepository(element,·repo,·isSelected)"⏎················` with `·hlmBtn·variant="outline"·class="flex·items-center·justify-between·w-full"·(click)="toggleRepository(element,·repo,·isSelected)"`
hlmBtn
variant="outline"
class="flex items-center justify-between w-full"
(click)="toggleRepository(element, repo, isSelected)"
>
<span>{{ repo }}</span>
@if(isSelected) {

Check failure on line 80 in webapp/src/app/admin/teams/table/teams-table.component.html

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Insert `·`
<hlm-icon class="w-4 h-4" name="lucideCheck" />
}
</button>
}
</hlm-scroll-area>
</div>
</brn-popover>
}
Expand All @@ -90,10 +103,13 @@
<hlm-icon class="w-4 h-4" name="lucidePlus" />
</button>
<div hlmPopoverContent class="w-80 p-4 gap-4 flex flex-wrap items-center justify-center" *brnPopoverContent="let ctx">
<input hlmInput placeholder="Label name" [formControl]="_newLabelName" />
<input hlmInput placeholder="Label name" [formControl]="_newLabelName" [class]="displayLabelAlert() ? 'border-destructive' : ''" />
<button hlmBtn size="icon" variant="default" class="ml-2" (click)="addLabel(element)">
<hlm-icon class="w-4 h-4" name="lucidePlus" />
</button>
@if (displayLabelAlert()) {
<div class="text-sm text-github-danger-foreground">Invalid label name</div>
}
</div>
</brn-popover>
}
Expand Down
59 changes: 34 additions & 25 deletions webapp/src/app/admin/teams/table/teams-table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { SelectionModel } from '@angular/cdk/collections';
import { Component, TrackByFunction, computed, effect, inject, input, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { lucideArrowUpDown, lucideChevronDown, lucideMoreHorizontal, lucideRotateCw, lucideXOctagon, lucidePlus } from '@ng-icons/lucide';
import { lucideArrowUpDown, lucideChevronDown, lucideMoreHorizontal, lucideRotateCw, lucideXOctagon, lucidePlus, lucideCheck } from '@ng-icons/lucide';
import { HlmButtonModule } from '@spartan-ng/ui-button-helm';
import { HlmIconComponent, provideIcons } from '@spartan-ng/ui-icon-helm';
import { HlmInputDirective } from '@spartan-ng/ui-input-helm';
Expand All @@ -19,12 +19,9 @@ import { AdminService, TeamInfo } from '@app/core/modules/openapi';
import { injectQueryClient } from '@tanstack/angular-query-experimental';
import { octNoEntry } from '@ng-icons/octicons';
import { HlmPopoverModule } from '@spartan-ng/ui-popover-helm';
import {
BrnPopoverComponent,
BrnPopoverContentDirective,
BrnPopoverTriggerDirective,
} from '@spartan-ng/ui-popover-brain';
import { BrnPopoverComponent, BrnPopoverContentDirective, BrnPopoverTriggerDirective } from '@spartan-ng/ui-popover-brain';
import { GithubLabelComponent } from '@app/ui/github-label/github-label.component';
import { HlmScrollAreaComponent } from '@spartan-ng/ui-scrollarea-helm';

const LOADING_TEAMS: TeamInfo[] = [
{
Expand Down Expand Up @@ -60,6 +57,7 @@ const LOADING_TEAMS: TeamInfo[] = [

HlmIconComponent,
HlmInputDirective,
HlmScrollAreaComponent,

BrnSelectModule,
HlmSelectModule,
Expand All @@ -74,7 +72,7 @@ const LOADING_TEAMS: TeamInfo[] = [

GithubLabelComponent
],
providers: [provideIcons({ lucideChevronDown, lucideMoreHorizontal, lucideArrowUpDown, lucideRotateCw, lucideXOctagon, lucidePlus })],
providers: [provideIcons({ lucideChevronDown, lucideMoreHorizontal, lucideArrowUpDown, lucideRotateCw, lucideXOctagon, lucidePlus, lucideCheck })],
templateUrl: './teams-table.component.html'
})
export class AdminTeamsTableComponent {
Expand All @@ -84,6 +82,7 @@ export class AdminTeamsTableComponent {

isLoading = input(false);
teamData = input.required<TeamInfo[] | undefined>();
allRepositories = input.required<Set<string> | undefined>();

_teams = computed(() => this.teamData() ?? LOADING_TEAMS);
protected readonly _rawFilterInput = signal('');
Expand Down Expand Up @@ -174,36 +173,25 @@ export class AdminTeamsTableComponent {
navigator.clipboard.writeText(element.name!);
}

_newRepositoryOwner = new FormControl('');
_newRepositoryName = new FormControl('');
_newLabelName = new FormControl('');
_newTeamName = new FormControl('');
_newTeamColor = new FormControl('');
displayLabelAlert = signal(false);

protected addLabel(team: TeamInfo) {
if (this.isLoading() || !this._newLabelName.value) {
return;
}
this.adminService.addLabelToTeam(team.id, this._newLabelName.value).subscribe({
next: () => {
this.displayLabelAlert.set(false);
this._newLabelName.reset();
this.invalidateTeams();
},
error: (err) => console.error('Error adding label', err),
});
}

protected addRepository(team: TeamInfo) {
if (this.isLoading() || !this._newRepositoryName.value) {
return;
}
this.adminService.addRepositoryToTeam(team.id, this._newRepositoryOwner.value!, this._newRepositoryName.value).subscribe({
next: () => {
this._newRepositoryOwner.reset();
this._newRepositoryName.reset();
this.invalidateTeams();
},
error: (err) => console.error('Error adding repository', err),
error: (err) => {
console.error('Error adding label', err);
this.displayLabelAlert.set(true);
}
});
}

Expand All @@ -213,7 +201,7 @@ export class AdminTeamsTableComponent {
}
const newTeam = {
name: this._newTeamName.value,
color: this._newTeamColor.value ?? '#000000',
color: this._newTeamColor.value ?? '#000000'
} as TeamInfo;
this.adminService.createTeam(newTeam).subscribe({
next: () => this.invalidateTeams(),
Expand All @@ -227,4 +215,25 @@ export class AdminTeamsTableComponent {
}
this.queryClient.invalidateQueries({ queryKey: ['admin', 'teams'] });
}

protected isRepositoryInTeam(team: TeamInfo, repository: string) {
return team.repositories.some((r) => r.nameWithOwner === repository);
}

protected toggleRepository(team: TeamInfo, repository: string, checked: boolean) {
const separatedName = repository.split('/');
if (checked) {
this.adminService.removeRepositoryFromTeam(team.id, separatedName[0], separatedName[1]).subscribe({
next: () => {
team.repositories = team.repositories.filter((r) => r.nameWithOwner !== repository);
},
error: (err) => console.error('Error removing repository', err)
});
} else {
this.adminService.addRepositoryToTeam(team.id, separatedName[0], separatedName[1]).subscribe({
next: (newTeam) => team.repositories.push(newTeam.repositories[newTeam.repositories.length - 1]),
error: (err) => console.error('Error adding repository', err)
});
}
}
}
11 changes: 9 additions & 2 deletions webapp/src/app/admin/teams/teams.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, inject } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { TeamService } from '@app/core/modules/openapi';
import { AdminService, TeamService } from '@app/core/modules/openapi';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { AdminTeamsTableComponent } from './table/teams-table.component';

Expand All @@ -10,14 +10,21 @@ import { AdminTeamsTableComponent } from './table/teams-table.component';
imports: [AdminTeamsTableComponent],
template: `
<h1 class="text-3xl font-bold mb-4">Teams</h1>
<app-admin-teams-table [teamData]="teamsQuery.data()" [isLoading]="teamsQuery.isPending() || teamsQuery.isRefetching()" />
<app-admin-teams-table [teamData]="teamsQuery.data()" [isLoading]="teamsQuery.isPending() || teamsQuery.isRefetching()" [allRepositories]="allReposQuery.data()" />
`
})
export class AdminTeamsComponent {
protected teamService = inject(TeamService);
protected adminService = inject(AdminService);

teamsQuery = injectQuery(() => ({
queryKey: ['admin', 'teams'],
queryFn: async () => lastValueFrom(this.teamService.getTeams())
}));

allReposQuery = injectQuery(() => ({
queryKey: ['admin', 'config'],
queryFn: async () => lastValueFrom(this.adminService.getConfig()),

Check failure on line 27 in webapp/src/app/admin/teams/teams.component.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Delete `·`
select: data => data.repositoriesToMonitor

Check failure on line 28 in webapp/src/app/admin/teams/teams.component.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Replace `data` with `(data)`
}));
}
Loading

0 comments on commit 0197821

Please sign in to comment.