Added short name to benchmarks

This commit is contained in:
ivan-pelly
2026-03-14 18:54:28 -07:00
parent f431fb3e94
commit 242b1bce27
21 changed files with 227 additions and 22 deletions
@@ -15,14 +15,18 @@
@if (loaded()) {
<div class="detail-card">
<div class="field">
<span class="field-label">Goal</span>
<span class="field-value">{{ goalCategory }}</span>
<span class="field-label">Goal: {{ goalCategory }}</span>
</div>
<div class="field">
<label class="field-label" for="benchmarkText">Benchmark</label>
<textarea id="benchmarkText" class="field-input field-textarea" [(ngModel)]="benchmarkText" rows="4"
placeholder="Enter benchmark text..."></textarea>
</div>
<div class="field">
<label class="field-label" for="shortName">Short Name</label>
<input id="shortName" class="field-input" type="text" [(ngModel)]="shortName" maxlength="50"
placeholder="Optional" />
</div>
@if (!isNew()) {
<div class="metadata">
@if (createdByName) {
@@ -42,9 +42,11 @@ export class BenchmarkCardFull implements OnDestroy {
protected readonly successMessage = signal<string | null>(null);
protected readonly saving = signal(false);
// Form field
// Form fields
protected benchmarkText = '';
protected shortName = '';
private savedBenchmarkText = '';
private savedShortName = '';
// Read-only metadata
protected goalCategory = '';
@@ -58,7 +60,8 @@ export class BenchmarkCardFull implements OnDestroy {
// Returns true if the benchmark text has unsaved changes.
// *****************************************************************
hasChanges(): boolean {
return this.benchmarkText !== this.savedBenchmarkText;
return this.benchmarkText !== this.savedBenchmarkText
|| this.shortName !== this.savedShortName;
}
// ************************ Public Methods *************************
@@ -77,11 +80,13 @@ export class BenchmarkCardFull implements OnDestroy {
const result = await this.studentService.createBenchmark(this.studentId, {
goalId: this.goalId,
benchmark: this.benchmarkText,
shortName: this.shortName || undefined,
});
this.saving.set(false);
if (result.success) {
this.successMessage.set('Benchmark created.');
this.savedBenchmarkText = this.benchmarkText;
this.savedShortName = this.shortName;
this.studentService.notifyDataChanged();
if (result.payload?.benchmarkId) {
this.router.navigate(['/students', this.studentId, 'goals', this.goalId, 'benchmarks', result.payload.benchmarkId]);
@@ -90,11 +95,19 @@ export class BenchmarkCardFull implements OnDestroy {
this.errorMessage.set(result.message);
}
} else {
const result = await this.studentService.updateBenchmark(this.studentId, this.benchmarkId!, this.benchmarkText);
const shortNameChanged = this.shortName !== this.savedShortName;
const result = await this.studentService.updateBenchmark(this.studentId, this.benchmarkId!, this.benchmarkText, this.shortName || undefined);
this.saving.set(false);
if (result.success) {
this.savedBenchmarkText = this.benchmarkText;
this.savedShortName = this.shortName;
this.successMessage.set('Changes saved.');
if (shortNameChanged) {
this.studentService.updateSidebarLabel(
['/students', this.studentId, 'goals', this.goalId, 'benchmarks', this.benchmarkId!],
this.shortName || this.benchmarkText
);
}
} else {
this.errorMessage.set(result.message);
}
@@ -106,12 +119,13 @@ export class BenchmarkCardFull implements OnDestroy {
// *****************************************************************
onCancel() {
this.benchmarkText = this.savedBenchmarkText;
this.shortName = this.savedShortName;
this.errorMessage.set(null);
this.successMessage.set(null);
}
onBack() {
this.router.navigate(['/students', this.studentId, 'benchmarks']);
this.router.navigate(['/students', this.studentId, 'goals', this.goalId, 'benchmarks']);
}
ngOnDestroy() {
@@ -127,7 +141,9 @@ export class BenchmarkCardFull implements OnDestroy {
if (!this.benchmarkId) {
this.isNew.set(true);
this.benchmarkText = '';
this.shortName = '';
this.savedBenchmarkText = '';
this.savedShortName = '';
this.loadGoalCategory();
this.loaded.set(true);
return;
@@ -147,7 +163,9 @@ export class BenchmarkCardFull implements OnDestroy {
}
this.benchmarkText = bm.benchmark;
this.shortName = bm.shortName ?? '';
this.savedBenchmarkText = bm.benchmark;
this.savedShortName = bm.shortName ?? '';
this.goalCategory = bm.goalCategory;
this.createdByName = bm.createdByName;
this.createdAt = bm.createdAt;
@@ -0,0 +1 @@
<p>progress-edit works!</p>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProgressEdit } from './progress-edit';
describe('ProgressEdit', () => {
let component: ProgressEdit;
let fixture: ComponentFixture<ProgressEdit>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProgressEdit]
})
.compileComponents();
fixture = TestBed.createComponent(ProgressEdit);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-progress-edit',
imports: [],
templateUrl: './progress-edit.html',
styleUrl: './progress-edit.scss',
})
export class ProgressEdit {
}
@@ -37,6 +37,11 @@ export class Home implements OnDestroy {
).subscribe(() => {
this.expandToRoute(this.router.url);
});
// Patch individual sidebar node labels without a full rebuild.
this.labelSub = this.studentService.sidebarLabelUpdate$.subscribe(update => {
this.patchNodeLabel(this.sidebarTree(), update.routerLink, update.label);
});
}
// ************************** Declarations *************************
@@ -45,6 +50,7 @@ export class Home implements OnDestroy {
private readonly router = inject(Router);
private readonly studentService = inject(StudentService);
private readonly routeSub: Subscription;
private readonly labelSub: Subscription;
protected readonly sidebarExpanded = signal(true);
protected readonly sidebarTree = signal<SidebarNode[]>([]);
@@ -68,10 +74,28 @@ export class Home implements OnDestroy {
ngOnDestroy() {
this.routeSub.unsubscribe();
this.labelSub.unsubscribe();
}
// ********************** Support Procedures ***********************
// *****************************************************************
// Recursively walks the sidebar tree to find a node whose
// routerLink matches the given link, and updates its label.
// *****************************************************************
private patchNodeLabel(nodes: SidebarNode[], routerLink: string[], label: string): boolean {
for (const node of nodes) {
if (node.routerLink && node.routerLink.join('/') === routerLink.join('/')) {
node.label = label;
return true;
}
if (node.children && this.patchNodeLabel(node.children, routerLink, label)) {
return true;
}
}
return false;
}
// *****************************************************************
// Loads student list, sorts by identifier, and builds the sidebar
// tree with lazy-loading callbacks for goals and benchmarks.
@@ -125,9 +149,8 @@ export class Home implements OnDestroy {
childCount: 2,
children: [
{
label: 'Progress Events',
label: goal.progressEventCount > 0 ? `Progress Events (${goal.progressEventCount})` : 'Progress Events',
routerLink: ['/students', studentId, 'goals', goal.goalId, 'progress'],
childCount: goal.progressEventCount,
},
{
label: 'Benchmarks',
@@ -152,7 +175,7 @@ export class Home implements OnDestroy {
return result.payload.benchmarks
.filter(b => b.goalId === goalId)
.map(b => ({
label: b.benchmark,
label: b.shortName || b.benchmark,
routerLink: ['/students', studentId, 'goals', goalId, 'benchmarks', b.benchmarkId],
}));
}