Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add pinning for prs and branches #433

Open
wants to merge 3 commits into
base: staging
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,45 @@
</ng-template>

<ng-template pTemplate="body" let-rowNode let-branch="rowData">
<tr [ttRow]="rowNode" class="cursor-pointer" (click)="openBranch(branch)">
<tr [class.bg-gray-100]="branch.isPinned" [ttRow]="rowNode">
@if (!rowNode.node.subheader) {
<td>
<div class="flex items-center gap-2">
<p-treeTableToggler [rowNode]="rowNode" />

<p-tag>
<div class="whitespace-normal inline-flex flex-wrap items-baseline">
@for (part of branch.name | highlight: searchTableService.searchValue(); track part.text) {
@if (part.highlight) {
<mark>{{ part.text }}</mark>
<td class="p-0">
<div class="flex h-full items-stretch gap-2">
<div
class="flex items-center border-r-4 pl-2"
[class]="branch.isPinned ? 'border-r-primary-700' : 'border-r-primary-300'"
(mouseenter)="isHovered.set(branch.name, true)"
(mouseleave)="isHovered.set(branch.name, false)"
>
<div class="align-middle">
@if (isHovered.get(branch.name)) {
@if (branch.isPinned) {
<p-button text (onClick)="setPinned($event, branch, false)" class="bg-transparent h-full">
<i-tabler name="pinned-off" class="!h-5 !w-5 text-primary-700 hover:text-primary-900" />
</p-button>
} @else {
<span>{{ part.text }}</span>
<p-button text (onClick)="setPinned($event, branch, true)" class="bg-transparent h-full">
<i-tabler name="pinned" class="!h-5 !w-5 text-primary-700 hover:text-primary-900" />
</p-button>
}
}
</div>
</p-tag>
</div>
<div class="flex items-center">
<p-treeTableToggler [rowNode]="rowNode" />

<p-tag class="cursor-pointer" (click)="openBranch(branch)">
<div class="whitespace-normal inline-flex flex-wrap items-baseline">
@for (part of branch.name | highlight: searchTableService.searchValue(); track part.text) {
@if (part.highlight) {
<mark>{{ part.text }}</mark>
} @else {
<span>{{ part.text }}</span>
}
}
</div>
</p-tag>
</div>
@if (branch.isProtected) {
<i-tabler name="shield-half" pTooltip="Protected Branch" />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Component, computed, inject } from '@angular/core';
import { TableModule } from 'primeng/table';
import { AvatarModule } from 'primeng/avatar';
import { TagModule } from 'primeng/tag';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { injectMutation, injectQuery, QueryClient } from '@tanstack/angular-query-experimental';
import { IconsModule } from 'icons.module';
import { SkeletonModule } from 'primeng/skeleton';
import { InputIconModule } from 'primeng/inputicon';
Expand All @@ -12,7 +12,11 @@ import { TreeTableModule } from 'primeng/treetable';
import { ButtonModule } from 'primeng/button';
import { BranchViewPreferenceService } from '@app/core/services/branches-table/branch-view-preference';
import { Router } from '@angular/router';
import { getAllBranchesOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
import {
getAllBranchesOptions,
getAllBranchesQueryKey,
setBranchPinnedByRepositoryIdAndNameAndUserIdMutation,
} from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
import { BranchInfoDto } from '@app/core/modules/openapi';
import { ProgressBarModule } from 'primeng/progressbar';
import { DividerModule } from 'primeng/divider';
Expand All @@ -23,6 +27,7 @@ import { FILTER_OPTIONS_TOKEN, SearchTableService } from '@app/core/services/sea
import { TableFilterComponent } from '../table-filter/table-filter.component';
import { WorkflowRunStatusComponent } from '@app/components/workflow-run-status-component/workflow-run-status.component';
import { HighlightPipe } from '@app/pipes/highlight.pipe';
import { MessageService } from 'primeng/api';

type BranchInfoWithLink = BranchInfoDto & { link: string; lastCommitLink: string };

Expand Down Expand Up @@ -83,14 +88,26 @@ const FILTER_OPTIONS = [
export class BranchTableComponent {
router = inject(Router);
viewPreference = inject(BranchViewPreferenceService);
messageService = inject(MessageService);
queryClient = inject(QueryClient);
searchTableService = inject(SearchTableService<BranchInfoWithLink>);

featureBranchesTree = computed(() => this.convertBranchesToTreeNodes(this.searchTableService.activeFilter().filter(this.branches())));

query = injectQuery(() => getAllBranchesOptions());
setPinnedMutation = injectMutation(() => ({
...setBranchPinnedByRepositoryIdAndNameAndUserIdMutation(),
onSuccess: () => {
this.messageService.add({ severity: 'success', summary: 'Pin Pull Request', detail: 'The pull request was pinned successfully' });
this.queryClient.invalidateQueries({ queryKey: getAllBranchesQueryKey() });
},
}));

// Use only branch name for map, because it is unique in this view
isHovered = new Map<string, boolean>();

globalFilterFields = ['name', 'commitSha'];

featureBranchesTree = computed(() => this.convertBranchesToTreeNodes(this.searchTableService.activeFilter().filter(this.branches())));

branches = computed<BranchInfoWithLink[]>(
() =>
this.query.data()?.map(branch => ({
Expand All @@ -117,6 +134,13 @@ export class BranchTableComponent {
}
}

setPinned(event: Event, branch: BranchInfoDto, isPinned: boolean): void {
event.stopPropagation();
if (!branch.repository) return;
this.setPinnedMutation.mutate({ path: { repoId: branch.repository.id }, query: { name: branch.name, isPinned } });
this.isHovered.set(branch.name, false);
}

convertBranchesToTreeNodes(branches: BranchInfoWithLink[]): TreeNode[] {
const rootNodes: TreeNode[] = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,47 @@
</tr>
</ng-template>
<ng-template pTemplate="body" let-pr>
<tr class="cursor-pointer" (click)="openPR(pr)">
<td>
<div class="flex flex-wrap items-center gap-2">
<app-pull-request-status-icon [pullRequest]="pr" tooltipPosition="top" />
<span class="font-bold"><span [innerHTML]="pr.title | markdown"></span></span>
<app-workflow-run-status [selector]="{ type: 'pullRequest', pullRequestId: pr.id }"></app-workflow-run-status>
<tr [class.bg-gray-100]="pr.isPinned">
<td class="p-0">
<div class="flex h-full items-stretch gap-2">
<div
class="flex items-center border-r-4 pl-2"
[class]="pr.isPinned ? 'border-r-primary-700' : 'border-r-primary-300'"
(mouseenter)="isHovered.set(pr.id, true)"
(mouseleave)="isHovered.set(pr.id, false)"
>
<div class="align-middle">
@if (isHovered.get(pr.id)) {
@if (pr.isPinned) {
<p-button text (onClick)="setPinned($event, pr, false)" class="bg-transparent h-full">
<i-tabler name="pinned-off" class="!h-5 !w-5 text-primary-700 hover:text-primary-900" />
</p-button>
} @else {
<p-button text (onClick)="setPinned($event, pr, true)" class="bg-transparent h-full">
<i-tabler name="pinned" class="!h-5 !w-5 text-primary-700 hover:text-primary-900" />
</p-button>
}
}
</div>
</div>

@for (label of pr.labels; track label.id) {
<p-tag [value]="label.name" [rounded]="true" class="border font-medium text-xs py-0.5 whitespace-nowrap" [style]="getLabelClasses(label.color)" />
}
</div>
<div class="flex mt-2.5 gap-2 items-center text-xs">
<p-avatar [image]="pr.author?.avatarUrl" shape="circle" class="h-5 w-5" [styleClass]="getAvatarBorderClass(pr.author?.login)" />
<span>{{ pr.author?.name }} opened #{{ pr.number }} {{ pr.createdAt | timeAgo }}</span>
<i-tabler name="point" class="!h-3 !w-3" />
<span>updated {{ pr.updatedAt | timeAgo }}</span>
<div class="flex flex-col gap-2.5">
<div class="flex flex-wrap items-center gap-2">
<app-pull-request-status-icon [pullRequest]="pr" tooltipPosition="top" />
<span class="font-bold cursor-pointer" (click)="openPR(pr)"><span [innerHTML]="pr.title | markdown"></span></span>
<app-workflow-run-status [selector]="{ type: 'pullRequest', pullRequestId: pr.id }"></app-workflow-run-status>

@for (label of pr.labels; track label.id) {
<p-tag [value]="label.name" [rounded]="true" class="border font-medium text-xs py-0.5 whitespace-nowrap" [style]="getLabelClasses(label.color)" />
}
</div>
<div class="flex gap-2 items-center text-xs">
<p-avatar [image]="pr.author?.avatarUrl" shape="circle" class="h-5 w-5" [styleClass]="getAvatarBorderClass(pr.author?.login)" />
<span>{{ pr.author?.name }} opened #{{ pr.number }} {{ pr.createdAt | timeAgo }}</span>
<i-tabler name="point" class="!h-3 !w-3" />
<span>updated {{ pr.updatedAt | timeAgo }}</span>
</div>
</div>
</div>
</td>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { Component, computed, inject } from '@angular/core';
import { TableModule } from 'primeng/table';
import { AvatarModule } from 'primeng/avatar';
import { TagModule } from 'primeng/tag';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { injectMutation, injectQuery, QueryClient } from '@tanstack/angular-query-experimental';
import { IconsModule } from 'icons.module';
import { SkeletonModule } from 'primeng/skeleton';
import { ActivatedRoute, Router } from '@angular/router';
import { DateService } from '@app/core/services/date.service';
import { getAllPullRequestsOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
import { getAllPullRequestsOptions, getAllPullRequestsQueryKey, setPrPinnedByNumberMutation } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
import { PullRequestBaseInfoDto, PullRequestInfoDto } from '@app/core/modules/openapi';
import { ButtonModule } from 'primeng/button';
import { DividerModule } from 'primeng/divider';
Expand All @@ -22,6 +22,7 @@ import { FILTER_OPTIONS_TOKEN, SearchTableService } from '@app/core/services/sea
import { TableFilterComponent } from '../table-filter/table-filter.component';
import { WorkflowRunStatusComponent } from '@app/components/workflow-run-status-component/workflow-run-status.component';
import { PullRequestStatusIconComponent } from '@app/components/pull-request-status-icon/pull-request-status-icon.component';
import { MessageService } from 'primeng/api';

const FILTER_OPTIONS = [
{ name: 'All pull requests', filter: (prs: PullRequestBaseInfoDto[]) => prs },
Expand Down Expand Up @@ -76,11 +77,22 @@ const FILTER_OPTIONS = [
export class PullRequestTableComponent {
dateService = inject(DateService);
searchTableService = inject(SearchTableService<PullRequestBaseInfoDto>);
messageService = inject(MessageService);
queryClient = inject(QueryClient);
router = inject(Router);
route = inject(ActivatedRoute);
keycloak = inject(KeycloakService);

query = injectQuery(() => getAllPullRequestsOptions());
setPinnedMutation = injectMutation(() => ({
...setPrPinnedByNumberMutation(),
onSuccess: () => {
this.messageService.add({ severity: 'success', summary: 'Pin Pull Request', detail: 'The pull request was pinned successfully' });
this.queryClient.invalidateQueries({ queryKey: getAllPullRequestsQueryKey() });
},
}));

isHovered = new Map<number, boolean>();

filteredPrs = computed(() => this.searchTableService.activeFilter().filter(this.query.data() || [], this.keycloak.decodedToken()?.preferred_username));

Expand Down Expand Up @@ -111,4 +123,10 @@ export class PullRequestTableComponent {
relativeTo: this.route.parent,
});
}

setPinned(event: Event, pr: PullRequestInfoDto, isPinned: boolean): void {
this.setPinnedMutation.mutate({ path: { pr: pr.id }, query: { isPinned } });
this.isHovered.set(pr.id, false);
event.stopPropagation();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import type {
CreateReleaseCandidateData,
CreateReleaseCandidateResponse,
EvaluateData,
SetPrPinnedByNumberData,
DeployToEnvironmentData,
DeployToEnvironmentResponse,
SetBranchPinnedByRepositoryIdAndNameAndUserIdData,
HealthCheckData,
GetAllWorkflowsData,
GetWorkflowByIdData,
Expand Down Expand Up @@ -64,7 +66,9 @@ import {
getAllReleaseCandidates,
createReleaseCandidate,
evaluate,
setPrPinnedByNumber,
deployToEnvironment,
setBranchPinnedByRepositoryIdAndNameAndUserId,
healthCheck,
getAllWorkflows,
getWorkflowById,
Expand Down Expand Up @@ -341,6 +345,37 @@ export const evaluateMutation = (options?: Partial<Options<EvaluateData>>) => {
return mutationOptions;
};

export const setPrPinnedByNumberQueryKey = (options: Options<SetPrPinnedByNumberData>) => [createQueryKey('setPrPinnedByNumber', options)];

export const setPrPinnedByNumberOptions = (options: Options<SetPrPinnedByNumberData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await setPrPinnedByNumber({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: setPrPinnedByNumberQueryKey(options),
});
};

export const setPrPinnedByNumberMutation = (options?: Partial<Options<SetPrPinnedByNumberData>>) => {
const mutationOptions: MutationOptions<unknown, DefaultError, Options<SetPrPinnedByNumberData>> = {
mutationFn: async localOptions => {
const { data } = await setPrPinnedByNumber({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};

export const deployToEnvironmentQueryKey = (options: Options<DeployToEnvironmentData>) => [createQueryKey('deployToEnvironment', options)];

export const deployToEnvironmentOptions = (options: Options<DeployToEnvironmentData>) => {
Expand Down Expand Up @@ -372,6 +407,39 @@ export const deployToEnvironmentMutation = (options?: Partial<Options<DeployToEn
return mutationOptions;
};

export const setBranchPinnedByRepositoryIdAndNameAndUserIdQueryKey = (options: Options<SetBranchPinnedByRepositoryIdAndNameAndUserIdData>) => [
createQueryKey('setBranchPinnedByRepositoryIdAndNameAndUserId', options),
];

export const setBranchPinnedByRepositoryIdAndNameAndUserIdOptions = (options: Options<SetBranchPinnedByRepositoryIdAndNameAndUserIdData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await setBranchPinnedByRepositoryIdAndNameAndUserId({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: setBranchPinnedByRepositoryIdAndNameAndUserIdQueryKey(options),
});
};

export const setBranchPinnedByRepositoryIdAndNameAndUserIdMutation = (options?: Partial<Options<SetBranchPinnedByRepositoryIdAndNameAndUserIdData>>) => {
const mutationOptions: MutationOptions<unknown, DefaultError, Options<SetBranchPinnedByRepositoryIdAndNameAndUserIdData>> = {
mutationFn: async localOptions => {
const { data } = await setBranchPinnedByRepositoryIdAndNameAndUserId({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};

export const healthCheckQueryKey = (options?: Options<HealthCheckData>) => [createQueryKey('healthCheck', options)];

export const healthCheckOptions = (options?: Options<HealthCheckData>) => {
Expand Down
7 changes: 5 additions & 2 deletions client/src/app/core/modules/openapi/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,9 @@ export const BranchInfoDtoSchema = {
isProtected: {
type: 'boolean',
},
isPinned: {
type: 'boolean',
},
updatedAt: {
type: 'string',
format: 'date-time',
Expand Down Expand Up @@ -719,8 +722,8 @@ export const PullRequestBaseInfoDtoSchema = {
isMerged: {
type: 'boolean',
},
repository: {
$ref: '#/components/schemas/RepositoryInfoDto',
isPinned: {
type: 'boolean',
},
htmlUrl: {
type: 'string',
Expand Down
Loading
Loading