Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
64c1952
Model & endpoint change
eliaspr Oct 1, 2025
1c49513
Config fix + migration
eliaspr Oct 1, 2025
fb8afca
Don#t store first time
eliaspr Oct 1, 2025
9cbaa1f
Add basic test
eliaspr Oct 3, 2025
e2a4052
Add endpoint
eliaspr Oct 3, 2025
f4d3ca4
Remove endpoint & include edit in application dto
eliaspr Oct 3, 2025
5f2d2b2
ApplicationChangeLog
eliaspr Oct 3, 2025
bc798e7
Add changelog when notes change
eliaspr Oct 3, 2025
3eec925
Add migration
eliaspr Oct 3, 2025
f6b0192
Create the change log entries
eliaspr Oct 3, 2025
4032f2f
Add Dto & endpoint
eliaspr Oct 3, 2025
6f34eeb
Add column & empty dialog for changelog
eliaspr Oct 3, 2025
a3521df
Load stuff from backend when dialog opens
eliaspr Oct 3, 2025
bdbbd71
Add TODO for implementing the html
eliaspr Oct 3, 2025
737ab4d
Merge branch 'main' into eliaspr/199-notes-history
eliaspr Oct 3, 2025
56aaf24
Fix linting
eliaspr Oct 4, 2025
274253a
Implement UI for displaying changes
eliaspr Oct 4, 2025
552d228
Remove spects
eliaspr Oct 4, 2025
8ba2fb7
Add new column for change log properties
eliaspr Oct 5, 2025
00b7527
Restructure backend to allow for more dynamic change logs
eliaspr Oct 5, 2025
fe6b4a2
Store as list + fix frontend
eliaspr Oct 5, 2025
2b025d6
Backend now working again
eliaspr Oct 5, 2025
41dfa98
Save label id
eliaspr Oct 5, 2025
0a9cac9
Add i18n
eliaspr Oct 5, 2025
a690abe
Icons
eliaspr Oct 5, 2025
6c2432b
DIsplay changelog
eliaspr Oct 5, 2025
fddbbd8
remove console log
eliaspr Oct 5, 2025
e3f7f1d
Separate component
eliaspr Oct 5, 2025
1c7cc95
Fix nullref
eliaspr Oct 5, 2025
d89a643
tests 1
eliaspr Oct 5, 2025
41d4912
tests 2
eliaspr Oct 5, 2025
229a4fb
test
eliaspr Oct 5, 2025
e7989d5
adjustments
eliaspr Oct 5, 2025
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
21 changes: 20 additions & 1 deletion src/Turnierplan.App/Client/src/app/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,26 @@ export const de = {
HiddenTeamsTooltip: 'Diese Anmeldung beinhaltet weitere Mannschaften, welche nicht den Filterkriterien entsprechen',
HiddenTeams: '{{count}} weitere Mannschaft(en) werden aufgrund Ihrer Filterkriterien nicht angezeigt.',
HiddenTeamsShowAll: 'alle anzeigen',
NoResults: 'Es gibt keine Anmeldungen, welche den angegebenen Filterkriterien entsprechen.'
ChangeLogTooltip: 'Bisherige Änderungen an dieser Anmeldung anzeigen',
NoResults: 'Es gibt keine Anmeldungen, welche den angegebenen Filterkriterien entsprechen.',
ChangeLog: {
Title: 'Änderungshistorie',
ApplicationCreated: 'Anmeldung erstellt',
Types: {
NotesChanged: 'Notizen bearbeitet',
ContactChanged: 'Kontaktperson geändert',
ContactEmailChanged: 'Kontakt E-Mail geändert',
ContactTelephoneChanged: 'Kontakt Telefon-Nr. geändert',
CommentChanged: 'Bemerkung geändert',
TeamAdded: 'Mannschaft hinzugefügt',
TeamRenamed: 'Mannschaft umbenannt',
TeamRemoved: 'Mannschaft entfernt',
LabelAdded: 'Label hinzugefügt',
LabelRemoved: 'Label entfernt'
},
LabelAdded: 'hinzugefügt zur Mannschaft',
LabelRemoved: 'entfernt von der Mannschaft'
}
},
Settings: {
Rename: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="d-flex flex-row align-items-center gap-3">
<i class="bi" [class]="changeLogIcon"></i>
<div class="flex-grow-1 d-flex flex-column gap-1 align-items-stretch">
<span class="small">
<span>#{{ index }}</span>
<span class="mx-1">&middot;</span>
<span>{{ changeLogTimestamp | translateDate }}</span>
<span class="mx-1">&middot;</span>
<span [translate]="'Portal.ViewPlanningRealm.Applications.ChangeLog.Types.' + changeLogType"></span>
</span>

@if (changeLogType === ApplicationChangeLogType.LabelAdded) {
<div class="d-flex flex-row align-items-center gap-1">
<tp-label [label]="mockLabel!" />
<span class="ms-1" [translate]="'Portal.ViewPlanningRealm.Applications.ChangeLog.LabelAdded'"></span>
<span class="text-decoration-underline">{{ teamName }}</span>
</div>
} @else if (changeLogType === ApplicationChangeLogType.LabelRemoved) {
<div class="d-flex flex-row align-items-center gap-1">
<tp-label [label]="mockLabel!" />
<span class="ms-1" [translate]="'Portal.ViewPlanningRealm.Applications.ChangeLog.LabelRemoved'"></span>
<span class="text-decoration-underline">{{ teamName }}</span>
</div>
} @else if (changeLogType === ApplicationChangeLogType.TeamAdded) {
<!-- TODO: Implement with the following issue: https://github.com/turnierplan-NET/turnierplan.NET/issues/192 -->
} @else if (changeLogType === ApplicationChangeLogType.TeamRemoved) {
<!-- TODO: Implement with the following issue: https://github.com/turnierplan-NET/turnierplan.NET/issues/192 -->
} @else {
<div class="d-flex flex-row align-items-start gap-2">
<span class="value-display p-1 border text-danger flex-grow-0">{{ previousValue }}</span>
<span class="mt-1 bi bi-arrow-right"></span>
<span class="value-display p-1 border text-success flex-grow-0">{{ newValue }}</span>
</div>
}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.value-display {
flex-basis: 50%;
min-height: calc(2rem + 2px); // ensure the correct height of only old value or new value is empty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Component, Input } from '@angular/core';
import { LabelComponent } from '../label/label.component';
import { TranslateDatePipe } from '../../pipes/translate-date.pipe';
import { ApplicationChangeLogType } from '../../../api/models/application-change-log-type';
import { ApplicationChangeLogProperty } from '../../../api/models/application-change-log-property';
import { ApplicationChangeLogDto } from '../../../api/models/application-change-log-dto';
import { LabelDto } from '../../../api/models/label-dto';
import { TranslateDirective } from '@ngx-translate/core';

@Component({
selector: 'tp-application-change-log-entry',
imports: [LabelComponent, TranslateDatePipe, TranslateDirective],
templateUrl: './application-change-log-entry.component.html',
styleUrl: './application-change-log-entry.component.scss'
})
export class ApplicationChangeLogEntryComponent {
@Input()
public index!: number;

@Input()
public set entry(value: ApplicationChangeLogDto) {
this.changeLogType = value.type;
this.changeLogTimestamp = value.timestamp;

const propertyValue = (type: ApplicationChangeLogProperty): string => {
return value.properties.find((x) => x.type === type)?.value ?? '';
};

switch (value.type) {
case ApplicationChangeLogType.NotesChanged:
case ApplicationChangeLogType.CommentChanged:
case ApplicationChangeLogType.ContactChanged:
case ApplicationChangeLogType.ContactEmailChanged:
case ApplicationChangeLogType.ContactTelephoneChanged:
case ApplicationChangeLogType.TeamRenamed: {
this.changeLogIcon = 'bi-pencil-square';
this.previousValue = propertyValue(ApplicationChangeLogProperty.PreviousValue);
this.newValue = propertyValue(ApplicationChangeLogProperty.NewValue);
break;
}
case ApplicationChangeLogType.TeamAdded: {
this.changeLogIcon = 'bi-plus-square';
this.teamName = propertyValue(ApplicationChangeLogProperty.TeamName);
break;
}
case ApplicationChangeLogType.TeamRemoved: {
this.changeLogIcon = 'bi-dash-square';
this.teamName = propertyValue(ApplicationChangeLogProperty.TeamName);
break;
}
case ApplicationChangeLogType.LabelAdded:
case ApplicationChangeLogType.LabelRemoved: {
this.changeLogIcon = 'bi-tags';
this.teamName = propertyValue(ApplicationChangeLogProperty.TeamName);
this.mockLabel = {
id: 0,
name: propertyValue(ApplicationChangeLogProperty.LabelName),
colorCode: propertyValue(ApplicationChangeLogProperty.LabelColorCode),
description: ''
};
break;
}
}
}

protected readonly ApplicationChangeLogType = ApplicationChangeLogType;

protected changeLogType?: ApplicationChangeLogType;
protected changeLogTimestamp: string = '';
protected changeLogIcon: string = '';

protected previousValue?: string;
protected newValue?: string;
protected teamName?: string;
protected mockLabel?: LabelDto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<ng-container *tpLoadingState="{ isLoading: isLoadingChangeLog }">
<div class="modal-header">
<div class="modal-title" translate="Portal.ViewPlanningRealm.Applications.ChangeLog.Title"></div>
<button type="button" class="btn-close" (click)="modal.close()"></button>
</div>
<div class="overflow-y-scroll p-3 gap-3 d-flex flex-column align-items-stretch">
@for (entry of changeLog; track entry.id; let index = $index) {
<tp-application-change-log-entry [index]="changeLog.length - index + 1" [entry]="entry" />
<hr class="my-0" style="opacity: 0.1" />
}

<div class="d-flex flex-row align-items-center gap-3">
<i class="bi bi-stars"></i>
<span class="small">
<span>#1</span>
<span class="mx-1">&middot;</span>
<span>{{ applicationCreatedAt | translateDate }}</span>
<span class="mx-1">&middot;</span>
<span [translate]="'Portal.ViewPlanningRealm.Applications.ChangeLog.ApplicationCreated'"></span>
</span>
</div>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host {
// Set min-height to prevent size reduction of the dialog upon switching to loading indicator.
// 480px is the combined height of the loading indicator and its y margin.
min-height: 480px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Component, OnDestroy } from '@angular/core';
import { ApplicationDto } from '../../../api/models/application-dto';
import { ApplicationChangeLogDto } from '../../../api/models/application-change-log-dto';
import { TurnierplanApi } from '../../../api/turnierplan-api';
import { Observable, Subject } from 'rxjs';
import { getApplicationChangeLog } from '../../../api/fn/applications/get-application-change-log';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateDirective } from '@ngx-translate/core';
import { LoadingStateDirective } from '../../directives/loading-state.directive';
import { TranslateDatePipe } from '../../pipes/translate-date.pipe';
import { ApplicationChangeLogEntryComponent } from '../application-change-log-entry/application-change-log-entry.component';

@Component({
selector: 'tp-application-change-log',
imports: [TranslateDirective, LoadingStateDirective, TranslateDatePipe, ApplicationChangeLogEntryComponent],
templateUrl: './application-change-log.component.html',
styleUrl: './application-change-log.component.scss'
})
export class ApplicationChangeLogComponent implements OnDestroy {
protected isLoadingChangeLog = true;
protected changeLog: ApplicationChangeLogDto[] = [];
protected applicationCreatedAt: string = '';

private readonly errorSubject$ = new Subject<unknown>();

constructor(
protected readonly modal: NgbActiveModal,
private readonly turnierplanApi: TurnierplanApi
) {}

public get error$(): Observable<unknown> {
return this.errorSubject$.asObservable();
}

public ngOnDestroy(): void {
this.errorSubject$.complete();
}

public init(planningRealmId: string, application: ApplicationDto): void {
this.applicationCreatedAt = application.createdAt;

this.turnierplanApi.invoke(getApplicationChangeLog, { planningRealmId: planningRealmId, applicationId: application.id }).subscribe({
next: (result) => {
this.changeLog = result;
this.changeLog.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
this.isLoadingChangeLog = false;
},
error: (error) => {
this.errorSubject$.next(error);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<span translate="Portal.ViewPlanningRealm.Applications.TableHeader.Notes"></span>
<tp-tooltip-icon [tooltipText]="'Portal.ViewPlanningRealm.Applications.TableHeader.NotesTooltip'" />
</th>
<th style="width: 1px"></th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -113,11 +114,17 @@
}
</div>
</td>
<td class="align-correctly">
<i
class="bi bi-clock-history tp-cursor-pointer"
[ngbTooltip]="'Portal.ViewPlanningRealm.Applications.ChangeLogTooltip' | translate"
(click)="showApplicationChangeLog(application)"></i>
</td>
</tr>

@if (showApplicationTeams) {
<tr>
<td colspan="10">
<td colspan="11">
<table style="max-width: 900px" class="mb-0 table table-sm table-bordered">
<thead>
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { LabelsSelectComponent } from '../labels-select/labels-select.component'
import { setApplicationTeamLabels } from '../../../api/fn/application-teams/set-application-team-labels';
import { Actions } from '../../../generated/actions';
import { AuthorizationService } from '../../../core/services/authorization.service';
import { ApplicationChangeLogComponent } from '../application-change-log/application-change-log.component';

@Component({
selector: 'tp-manage-applications',
Expand Down Expand Up @@ -227,6 +228,25 @@ export class ManageApplicationsComponent implements OnDestroy {
});
}

protected showApplicationChangeLog(application: ApplicationDto): void {
const ref = this.modalService.open(ApplicationChangeLogComponent, {
centered: true,
size: 'lg',
fullscreen: 'lg',
scrollable: true
});

const component = ref.componentInstance as ApplicationChangeLogComponent;
component.init(this.planningRealm.id, application);

component.error$.subscribe({
next: (value) => {
ref.close();
this.errorOccured.emit(value);
}
});
}

protected navigateToTournament(tournamentId: PublicId): void {
this.localStorageService.setNavigationTab(tournamentId, ViewTournamentComponent.TeamListPageId);
void this.router.navigate(['/portal/tournament/', tournamentId]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@ private static async Task<IResult> Handle(
return Results.NotFound();
}

// The code becomes simpler if we just clear all labels and re-add the desired ones
applicationTeam.RemoveAllLabels();
var labelsToAdd = request.LabelIds.Where(id => !applicationTeam.Labels.Any(label => label.Id == id)).ToList();
var labelsToRemove = applicationTeam.Labels.Where(label => !request.LabelIds.Contains(label.Id)).ToList();

foreach (var labelId in request.LabelIds)
foreach (var label in labelsToRemove)
{
applicationTeam.RemoveLabel(label);
}

foreach (var labelId in labelsToAdd)
{
var label = planningRealm.Labels.FirstOrDefault(x => x.Id == labelId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ private static async Task<IResult> Handle(
}
}

// don't add change log entries for the previously changed properties & added teams
application.ClearChangeLog();

await planningRealmRepository.UnitOfWork.SaveChangesAsync(cancellationToken);

return Results.Ok(mapper.Map<ApplicationDto>(application));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using Turnierplan.App.Mapping;
using Turnierplan.App.Models;
using Turnierplan.App.Security;
using Turnierplan.Core.PlanningRealm;
using Turnierplan.Core.PublicId;

namespace Turnierplan.App.Endpoints.Applications;

internal sealed class GetApplicationChangeLogEndpoint : EndpointBase<IEnumerable<ApplicationChangeLogDto>>
{
protected override HttpMethod Method => HttpMethod.Get;

protected override string Route => "/api/planning-realms/{planningRealmId}/applications/{applicationId:int}/changelog";

protected override Delegate Handler => Handle;

private static async Task<IResult> Handle(
[FromRoute] PublicId planningRealmId,
[FromRoute] long applicationId,
IPlanningRealmRepository planningRealmRepository,
IAccessValidator accessValidator,
IApplicationChangeLogRepository applicationChangeLogRepository,
IMapper mapper)
{
var planningRealm = await planningRealmRepository.GetByPublicIdAsync(planningRealmId, IPlanningRealmRepository.Includes.Applications);

if (planningRealm is null)
{
return Results.NotFound();
}

if (!accessValidator.IsActionAllowed(planningRealm, Actions.ApplicationsRead))
{
return Results.Forbid();
}

var application = planningRealm.Applications.FirstOrDefault(x => x.Id == applicationId);

if (application is null)
{
return Results.NotFound();
}

var changeLog = await applicationChangeLogRepository.GetByApplicationIdAsync(application.Id);

// the oldest change log comes first in the result list
changeLog.Sort((x, y) => Math.Sign(x.Timestamp.Ticks - y.Timestamp.Ticks));

return Results.Ok(mapper.MapCollection<ApplicationChangeLogDto>(changeLog));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ private static async Task<IResult> Handle(
return Results.NotFound();
}

application.Notes = request.Notes.Trim();
application.Notes = request.Notes;

await planningRealmRepository.UnitOfWork.SaveChangesAsync(cancellationToken);

Expand Down
Loading
Loading