Skip to content
Merged
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
4 changes: 4 additions & 0 deletions resources/data/changelog.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
}
],
"fixes": [
{
"title": "Single Funding Reference Add Action",
"description": "The Data Editor now shows one clear Add Funding Reference action when the section is empty instead of displaying the same prompt twice. The empty-state action creates the first reference, while a single list-level button remains available for adding further references."
},
{
"title": "Complete Datacenter Template Assignments",
"description": "Admins and Group Leaders can now assign GFZ and every other datacenter to custom Resource and IGSN landing-page templates independently. A custom template may serve zero, one, or many datacenters; moving or removing an assignment preserves the other template type and automatically restores the matching Templates Resources or Templates IGSN fallback. New datacenters and existing gaps receive both persistent defaults."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,12 @@ export function FundingReferenceField({ value = [], onChange }: FundingReference
)}

{/* Add Button */}
<Button type="button" variant="outline" size="sm" onClick={handleAdd} className="w-full">
<Plus className="mr-2 h-4 w-4" />
Add Funding Reference
</Button>
{value.length > 0 && (
<Button type="button" variant="outline" size="sm" onClick={handleAdd} className="w-full">
<Plus className="mr-2 h-4 w-4" />
Add Funding Reference
</Button>
)}
</div>
);
}
7 changes: 7 additions & 0 deletions tests/pest/Feature/Api/ChangelogApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
it('returns changelog data grouped by release', function () {
getJson('/api/changelog')
->assertOk()
->assertJsonFragment([
'version' => '1.0.8',
'date' => '2026-09-09',
])
->assertJsonFragment([
'title' => 'Single Funding Reference Add Action',
])
->assertJsonFragment([
'version' => '1.0.7',
'date' => '2026-09-04',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ describe('FundingReferenceField', () => {
});
});

it('shows exactly one add action when no funding references exist', async () => {
render(<FundingReferenceField value={[]} onChange={onChange} />);

await waitFor(() => {
expect(screen.queryByText(/loading ror data/i)).not.toBeInTheDocument();
});

expect(screen.getByRole('button', { name: /^add funding reference$/i })).toBeInTheDocument();
});

it('does not show an artificial maximum', async () => {
render(<FundingReferenceField value={[]} onChange={onChange} />);

Expand Down Expand Up @@ -139,8 +149,8 @@ describe('FundingReferenceField', () => {
expect(screen.queryByText(/loading ror data/i)).not.toBeInTheDocument();
});

const addButtons = screen.getAllByRole('button', { name: /add funding reference/i });
await user.click(addButtons[0]);
const addButton = screen.getByRole('button', { name: /^add funding reference$/i });
await user.click(addButton);

expect(onChange).toHaveBeenCalledWith([
expect.objectContaining({
Expand All @@ -163,7 +173,7 @@ describe('FundingReferenceField', () => {
expect(screen.queryByText(/loading ror data/i)).not.toBeInTheDocument();
});

const [addButton] = screen.getAllByRole('button', { name: /add funding reference/i });
const addButton = screen.getByRole('button', { name: /^add funding reference$/i });
await user.click(addButton);
await user.click(addButton);

Expand All @@ -179,6 +189,28 @@ describe('FundingReferenceField', () => {
}
});

it('shows one add action for existing references and appends another reference', async () => {
const user = userEvent.setup();
const fundings = [createFunding({ id: 'f1', funderName: 'DFG' })];

render(<FundingReferenceField value={fundings} onChange={onChange} />);

await waitFor(() => {
expect(screen.queryByText(/loading ror data/i)).not.toBeInTheDocument();
});

await user.click(screen.getByRole('button', { name: /^add funding reference$/i }));

expect(onChange).toHaveBeenCalledWith([
expect.objectContaining({ id: 'f1', funderName: 'DFG' }),
expect.objectContaining({
id: expect.stringMatching(/^funding-[0-9a-f-]{36}$/i),
funderName: '',
isExpanded: false,
}),
]);
});

it('allows adding beyond the former maximum', async () => {
const legacySizedFundings = Array.from({ length: 100 }, (_, i) =>
createFunding({ id: `f-${i}`, funderName: `Funder ${i}` }),
Expand Down Expand Up @@ -214,6 +246,25 @@ describe('FundingReferenceField', () => {
expect.objectContaining({ id: 'f2', funderName: 'EU' }),
]);
});

it('returns to one empty-state add action after the last reference is removed', async () => {
const user = userEvent.setup();
const { rerender } = render(
<FundingReferenceField value={[createFunding({ id: 'f1', funderName: 'DFG' })]} onChange={onChange} />,
);

await waitFor(() => {
expect(screen.queryByText(/loading ror data/i)).not.toBeInTheDocument();
});

await user.click(screen.getByTestId('remove-funding-0'));
expect(onChange).toHaveBeenLastCalledWith([]);

rerender(<FundingReferenceField value={[]} onChange={onChange} />);

expect(screen.getByTestId('funding-empty-state')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /^add funding reference$/i })).toBeInTheDocument();
});
});

describe('field changes', () => {
Expand Down
Loading