diff --git a/resources/data/changelog.json b/resources/data/changelog.json index c86d2f4f3..420a67963 100644 --- a/resources/data/changelog.json +++ b/resources/data/changelog.json @@ -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." diff --git a/resources/js/components/curation/fields/funding-reference/funding-reference-field.tsx b/resources/js/components/curation/fields/funding-reference/funding-reference-field.tsx index 1533c1c6b..afdf0f18c 100644 --- a/resources/js/components/curation/fields/funding-reference/funding-reference-field.tsx +++ b/resources/js/components/curation/fields/funding-reference/funding-reference-field.tsx @@ -162,10 +162,12 @@ export function FundingReferenceField({ value = [], onChange }: FundingReference )} {/* Add Button */} - + {value.length > 0 && ( + + )} ); } diff --git a/tests/pest/Feature/Api/ChangelogApiTest.php b/tests/pest/Feature/Api/ChangelogApiTest.php index 4ac0d682d..8745ff940 100644 --- a/tests/pest/Feature/Api/ChangelogApiTest.php +++ b/tests/pest/Feature/Api/ChangelogApiTest.php @@ -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', diff --git a/tests/vitest/components/curation/fields/funding-reference/__tests__/funding-reference-field.test.tsx b/tests/vitest/components/curation/fields/funding-reference/__tests__/funding-reference-field.test.tsx index a62405278..fc0425f16 100644 --- a/tests/vitest/components/curation/fields/funding-reference/__tests__/funding-reference-field.test.tsx +++ b/tests/vitest/components/curation/fields/funding-reference/__tests__/funding-reference-field.test.tsx @@ -100,6 +100,16 @@ describe('FundingReferenceField', () => { }); }); + it('shows exactly one add action when no funding references exist', async () => { + render(); + + 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(); @@ -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({ @@ -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); @@ -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(); + + 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}` }), @@ -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( + , + ); + + await waitFor(() => { + expect(screen.queryByText(/loading ror data/i)).not.toBeInTheDocument(); + }); + + await user.click(screen.getByTestId('remove-funding-0')); + expect(onChange).toHaveBeenLastCalledWith([]); + + rerender(); + + expect(screen.getByTestId('funding-empty-state')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^add funding reference$/i })).toBeInTheDocument(); + }); }); describe('field changes', () => {