Skip to content

Commit 1843c1c

Browse files
committed
fix(funder): fixed bug with selected funder
1 parent a2b66b4 commit 1843c1c

File tree

3 files changed

+135
-43
lines changed

3 files changed

+135
-43
lines changed

src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<p-select
1111
[id]="'funderName-' + $index"
1212
formControlName="funderName"
13-
[options]="fundersList()"
13+
[options]="getOptionsForIndex($index)"
1414
optionLabel="name"
1515
optionValue="name"
1616
[placeholder]="'project.metadata.funding.dialog.selectFunder' | translate"

src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts

Lines changed: 114 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { Store } from '@ngxs/store';
2+
13
import { MockProvider, MockProviders } from 'ng-mocks';
24

35
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
46

5-
import { DestroyRef } from '@angular/core';
6-
import { ComponentFixture, TestBed } from '@angular/core/testing';
7+
import { DestroyRef, signal } from '@angular/core';
8+
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
79

810
import { RorFunderOption } from '../../models/ror.model';
9-
import { MetadataSelectors } from '../../store';
11+
import { GetFundersList, MetadataSelectors } from '../../store';
1012

1113
import { FundingDialogComponent } from './funding-dialog.component';
1214

@@ -43,22 +45,15 @@ describe('FundingDialogComponent', () => {
4345
expect(component).toBeTruthy();
4446
});
4547

46-
it('should add funding entry', () => {
47-
const initialLength = component.fundingEntries.length;
48-
component.addFundingEntry();
49-
50-
expect(component.fundingEntries.length).toBe(initialLength + 1);
51-
const entry = component.fundingEntries.at(component.fundingEntries.length - 1);
52-
expect(entry.get('funderName')?.value).toBe(null);
53-
expect(entry.get('awardTitle')?.value).toBe('');
54-
});
55-
56-
it('should not remove funding entry when only one exists', () => {
48+
it('should not remove last funding entry and close dialog with empty result', () => {
49+
const dialogRef = TestBed.inject(DynamicDialogRef);
50+
const closeSpy = jest.spyOn(dialogRef, 'close');
5751
expect(component.fundingEntries.length).toBe(1);
5852

5953
component.removeFundingEntry(0);
6054

6155
expect(component.fundingEntries.length).toBe(1);
56+
expect(closeSpy).toHaveBeenCalledWith({ fundingEntries: [] });
6257
});
6358

6459
it('should save valid form data', () => {
@@ -165,13 +160,6 @@ describe('FundingDialogComponent', () => {
165160
expect(component.fundingEntries.length).toBe(1);
166161
});
167162

168-
it('should not remove funding entry when only one exists', () => {
169-
expect(component.fundingEntries.length).toBe(1);
170-
171-
component.removeFundingEntry(0);
172-
expect(component.fundingEntries.length).toBe(1);
173-
});
174-
175163
it('should not remove funding entry when index is out of bounds', () => {
176164
component.addFundingEntry();
177165
const initialLength = component.fundingEntries.length;
@@ -239,32 +227,117 @@ describe('FundingDialogComponent', () => {
239227
expect(entry.get('awardNumber')?.value).toBe('');
240228
});
241229

242-
it('should emit search query to searchSubject', () => {
243-
const searchSpy = jest.spyOn(component['searchSubject'], 'next');
244-
245-
component.onFunderSearch('test search');
246-
247-
expect(searchSpy).toHaveBeenCalledWith('test search');
230+
it('should dispatch getFundersList after debounce when searching', fakeAsync(() => {
231+
const store = TestBed.inject(Store);
232+
const dispatchSpy = jest.spyOn(store, 'dispatch');
233+
234+
component.onFunderSearch('query');
235+
expect(dispatchSpy).not.toHaveBeenCalled();
236+
tick(300);
237+
expect(dispatchSpy).toHaveBeenCalledWith(new GetFundersList('query'));
238+
}));
239+
240+
it('should pre-populate entries from config funders on init', () => {
241+
TestBed.resetTestingModule();
242+
const configFunders = [
243+
{
244+
funderName: 'NSF',
245+
funderIdentifier: 'https://ror.org/nsf',
246+
funderIdentifierType: 'ROR',
247+
awardTitle: 'Grant A',
248+
awardUri: 'https://example.com/a',
249+
awardNumber: '123',
250+
},
251+
];
252+
TestBed.configureTestingModule({
253+
imports: [FundingDialogComponent, OSFTestingModule],
254+
providers: [
255+
MockProviders(DynamicDialogRef, DestroyRef),
256+
MockProvider(DynamicDialogConfig, { data: { funders: configFunders } }),
257+
provideMockStore({
258+
signals: [
259+
{ selector: MetadataSelectors.getFundersList, value: [] },
260+
{ selector: MetadataSelectors.getFundersLoading, value: false },
261+
],
262+
}),
263+
],
264+
}).compileComponents();
265+
const f = TestBed.createComponent(FundingDialogComponent);
266+
f.detectChanges();
267+
const c = f.componentInstance;
268+
expect(c.fundingEntries.length).toBe(1);
269+
const entry = c.fundingEntries.at(0);
270+
expect(entry.get('funderName')?.value).toBe('NSF');
271+
expect(entry.get('funderIdentifier')?.value).toBe('https://ror.org/nsf');
272+
expect(entry.get('funderIdentifierType')?.value).toBe('ROR');
273+
expect(entry.get('awardTitle')?.value).toBe('Grant A');
274+
expect(entry.get('awardUri')?.value).toBe('https://example.com/a');
275+
expect(entry.get('awardNumber')?.value).toBe('123');
248276
});
249277

250-
it('should handle empty search term', () => {
251-
const searchSpy = jest.spyOn(component['searchSubject'], 'next');
252-
253-
component.onFunderSearch('');
278+
it('getOptionsForIndex returns custom option plus list when entry name is not in list', () => {
279+
const entry = component.fundingEntries.at(0);
280+
entry.patchValue({ funderName: 'Custom Funder', funderIdentifier: 'custom-id' });
281+
const options = component.getOptionsForIndex(0);
282+
expect(options).toHaveLength(2);
283+
expect(options[0]).toEqual({ id: 'custom-id', name: 'Custom Funder' });
284+
expect(options[1]).toEqual(MOCK_ROR_FUNDERS[0]);
285+
});
254286

255-
expect(searchSpy).toHaveBeenCalledWith('');
287+
it('getOptionsForIndex returns list when entry has no name', () => {
288+
const options = component.getOptionsForIndex(0);
289+
expect(options).toEqual(MOCK_ROR_FUNDERS);
256290
});
257291

258-
it('should handle multiple search calls', () => {
259-
const searchSpy = jest.spyOn(component['searchSubject'], 'next');
292+
it('filterMessage returns loading key when funders loading', () => {
293+
TestBed.resetTestingModule();
294+
const loadingSignal = signal(true);
295+
TestBed.configureTestingModule({
296+
imports: [FundingDialogComponent, OSFTestingModule],
297+
providers: [
298+
MockProviders(DynamicDialogRef, DestroyRef),
299+
MockProvider(DynamicDialogConfig, { data: { funders: [] } }),
300+
provideMockStore({
301+
signals: [
302+
{ selector: MetadataSelectors.getFundersList, value: [] },
303+
{ selector: MetadataSelectors.getFundersLoading, value: loadingSignal },
304+
],
305+
}),
306+
],
307+
}).compileComponents();
308+
const f = TestBed.createComponent(FundingDialogComponent);
309+
f.detectChanges();
310+
expect(f.componentInstance.filterMessage()).toBe('project.metadata.funding.dialog.loadingFunders');
311+
loadingSignal.set(false);
312+
expect(f.componentInstance.filterMessage()).toBe('project.metadata.funding.dialog.noFundersFound');
313+
});
260314

261-
component.onFunderSearch('first');
262-
component.onFunderSearch('second');
263-
component.onFunderSearch('third');
315+
it('save returns only entries with at least one of funderName, awardTitle, awardUri, awardNumber', () => {
316+
const dialogRef = TestBed.inject(DynamicDialogRef);
317+
const closeSpy = jest.spyOn(dialogRef, 'close');
318+
component.addFundingEntry();
319+
component.fundingEntries.at(0).patchValue({ funderName: 'Funder A', awardTitle: 'Award A' });
320+
component.fundingEntries.at(1).patchValue({ funderName: 'Funder B', awardTitle: 'Award B' });
321+
fixture.detectChanges();
322+
component.save();
323+
expect(closeSpy).toHaveBeenCalledWith({
324+
fundingEntries: [
325+
expect.objectContaining({ funderName: 'Funder A', awardTitle: 'Award A' }),
326+
expect.objectContaining({ funderName: 'Funder B', awardTitle: 'Award B' }),
327+
],
328+
});
329+
});
264330

265-
expect(searchSpy).toHaveBeenCalledTimes(3);
266-
expect(searchSpy).toHaveBeenNthCalledWith(1, 'first');
267-
expect(searchSpy).toHaveBeenNthCalledWith(2, 'second');
268-
expect(searchSpy).toHaveBeenNthCalledWith(3, 'third');
331+
it('should not save when awardUri is invalid', () => {
332+
const dialogRef = TestBed.inject(DynamicDialogRef);
333+
const closeSpy = jest.spyOn(dialogRef, 'close');
334+
const entry = component.fundingEntries.at(0);
335+
entry.patchValue({
336+
funderName: 'Test Funder',
337+
awardUri: 'not-a-valid-url',
338+
});
339+
fixture.detectChanges();
340+
component.save();
341+
expect(closeSpy).not.toHaveBeenCalled();
269342
});
270343
});

src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } fr
1515

1616
import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper';
1717

18-
import { Funder, FundingDialogResult, FundingEntryForm, FundingForm, SupplementData } from '../../models';
18+
import {
19+
Funder,
20+
FundingDialogResult,
21+
FundingEntryForm,
22+
FundingForm,
23+
RorFunderOption,
24+
SupplementData,
25+
} from '../../models';
1926
import { GetFundersList, MetadataSelectors } from '../../store';
2027

2128
@Component({
@@ -99,6 +106,18 @@ export class FundingDialogComponent implements OnInit {
99106
});
100107
}
101108

109+
getOptionsForIndex(index: number): RorFunderOption[] {
110+
const list = this.fundersList() ?? [];
111+
const entry = this.fundingEntries.at(index);
112+
const name = entry?.get('funderName')?.value;
113+
114+
if (!name || list.some((f) => f.name === name)) {
115+
return list;
116+
}
117+
118+
return [{ id: entry?.get('funderIdentifier')?.value ?? '', name }, ...list];
119+
}
120+
102121
addFundingEntry(supplement?: SupplementData): void {
103122
const entry = this.createFundingEntryGroup(supplement);
104123
this.fundingEntries.push(entry);

0 commit comments

Comments
 (0)