feat(library): add the merge duplicate dialog
Build, Test & Push Frontend / quality-check (push) Successful in 18s
Build, Test & Push Frontend / docker (push) Successful in 24s

This commit is contained in:
lobo committed 2026-09-28 11:25:45 +02:00
1 parent 01ae372b2d
commit 78ba0700c8
10 files changed
+355 -84

No files matched your search

+15 -1
View File
@@ -1,7 +1,7 @@
import {useEffect, useMemo, useState} from 'react';
import {Link, useNavigate, useParams} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {ArrowLeft, Clock, ExternalLink, Eye, EyeOff, Gamepad2, Image as ImageIcon, ImageOff, PanelTop, PencilLine, RotateCcw, ShieldOff, Timer, Trophy} from 'lucide-react';
import {ArrowLeft, Clock, ExternalLink, Eye, EyeOff, Gamepad2, Image as ImageIcon, ImageOff, Merge, PanelTop, PencilLine, RotateCcw, ShieldOff, Timer, Trophy} from 'lucide-react';
import {toast} from 'sonner';
import {Badge} from '@/components/ui/badge';
@@ -16,6 +16,7 @@ import ManualGameCard from '../components/ManualGameCard.jsx';
import CompletedHint from '../components/CompletedHint.jsx';
import FinishedHint from '../components/FinishedHint.jsx';
import GameCorrectionDialog from '../components/GameCorrectionDialog.jsx';
import GameMergeDialog from '../components/GameMergeDialog.jsx';
import PlayingHint from '../components/PlayingHint.jsx';
import TimeToBeat from '../components/TimeToBeat.jsx';
import {formatPlaytime} from '../utils/games.js';
@@ -67,6 +68,7 @@ export default function GameDetail() {
const [hiding, setHiding] = useState(false);
const [error, setError] = useState(null);
const [correcting, setCorrecting] = useState(false);
const [merging, setMerging] = useState(false);
const [reloads, setReloads] = useState(0);
const auditor = canCorrect(useCurrentUser(true));
@@ -222,6 +224,11 @@ export default function GameDetail() {
<PencilLine/>{t('CORRECTION.OPEN')}
</Button>
)}
{auditor && (
<Button variant="outline" size="sm" className="mt-3 ml-2" onClick={() => setMerging(true)}>
<Merge/>{t('MERGE.OPEN')}
</Button>
)}
</div>
{/* Store page, background and banner need Steam art, so a game off Steam has none of them.
Hide is here too: only a Steam game can be hidden. */}
@@ -355,6 +362,13 @@ export default function GameDetail() {
<GameCorrectionDialog game={data} open={correcting} onOpenChange={setCorrecting}
onChanged={() => setReloads((n) => n + 1)}/>
)}
{auditor && data && (
<GameMergeDialog game={data} open={merging} onOpenChange={setMerging} onMerged={(merged) => {
// A new key loads through the address; the same key needs a reload.
if (merged === data.appid) setReloads((n) => n + 1);
else navigate(`/library/${merged}`, {replace: true});
}}/>
)}
</PageContainer>
);
}
+10
View File
@@ -257,6 +257,15 @@ describe('GameDetail of a game added by hand', () => {
expect(await screen.findByText('CORRECTION.TITLE')).toBeTruthy();
});
it('offers "merge duplicate" to an active auditor', async () => {
loggedInAs({role: 'AUDITOR', activeRole: 'AUDITOR'});
renderDetail();
fireEvent.click(await screen.findByRole('button', {name: 'MERGE.OPEN'}));
expect(await screen.findByText('MERGE.TITLE')).toBeTruthy();
});
it('offers nothing to a user or to an admin viewing as user', async () => {
for (const me of [{role: 'USER', activeRole: 'USER'}, {role: 'PLATFORM_ADMIN', activeRole: 'USER'}]) {
loggedInAs(me);
@@ -267,6 +276,7 @@ describe('GameDetail of a game added by hand', () => {
// Let the answer of /me reach the page before looking for the button.
await act(async () => {});
expect(screen.queryByRole('button', {name: 'CORRECTION.OPEN'})).toBeNull();
expect(screen.queryByRole('button', {name: 'MERGE.OPEN'})).toBeNull();
cleanup();
}
});