diff --git a/.beans/nuzlocke-tracker-z511--click-to-edit-pattern-across-admin-tables.md b/.beans/nuzlocke-tracker-z511--click-to-edit-pattern-across-admin-tables.md new file mode 100644 index 0000000..571091f --- /dev/null +++ b/.beans/nuzlocke-tracker-z511--click-to-edit-pattern-across-admin-tables.md @@ -0,0 +1,11 @@ +--- +# nuzlocke-tracker-z511 +title: Click-to-edit pattern across admin tables +status: completed +type: feature +priority: normal +created_at: 2026-02-08T12:39:48Z +updated_at: 2026-02-08T12:44:09Z +--- + +Remove Actions columns from all admin tables. Row click opens edit modal. Delete moves into edit modal footer with inline two-step confirm. See plan for full details. \ No newline at end of file diff --git a/frontend/src/components/admin/BossBattleFormModal.tsx b/frontend/src/components/admin/BossBattleFormModal.tsx index b297a88..88ec4fc 100644 --- a/frontend/src/components/admin/BossBattleFormModal.tsx +++ b/frontend/src/components/admin/BossBattleFormModal.tsx @@ -10,6 +10,9 @@ interface BossBattleFormModalProps { onSubmit: (data: CreateBossBattleInput | UpdateBossBattleInput) => void onClose: () => void isSubmitting?: boolean + onDelete?: () => void + isDeleting?: boolean + onEditTeam?: () => void } const BOSS_TYPES = [ @@ -28,6 +31,9 @@ export function BossBattleFormModal({ onSubmit, onClose, isSubmitting, + onDelete, + isDeleting, + onEditTeam, }: BossBattleFormModalProps) { const [name, setName] = useState(boss?.name ?? '') const [bossType, setBossType] = useState(boss?.bossType ?? 'gym_leader') @@ -63,6 +69,17 @@ export function BossBattleFormModal({ onClose={onClose} onSubmit={handleSubmit} isSubmitting={isSubmitting} + onDelete={onDelete} + isDeleting={isDeleting} + headerExtra={onEditTeam ? ( + + ) : undefined} >
diff --git a/frontend/src/components/admin/EvolutionFormModal.tsx b/frontend/src/components/admin/EvolutionFormModal.tsx index ed49021..64a6f54 100644 --- a/frontend/src/components/admin/EvolutionFormModal.tsx +++ b/frontend/src/components/admin/EvolutionFormModal.tsx @@ -8,6 +8,8 @@ interface EvolutionFormModalProps { onSubmit: (data: CreateEvolutionInput | UpdateEvolutionInput) => void onClose: () => void isSubmitting?: boolean + onDelete?: () => void + isDeleting?: boolean } const TRIGGER_OPTIONS = ['level-up', 'trade', 'use-item', 'shed', 'other'] @@ -17,6 +19,8 @@ export function EvolutionFormModal({ onSubmit, onClose, isSubmitting, + onDelete, + isDeleting, }: EvolutionFormModalProps) { const [fromPokemonId, setFromPokemonId] = useState( evolution?.fromPokemonId ?? null, @@ -52,6 +56,8 @@ export function EvolutionFormModal({ onClose={onClose} onSubmit={handleSubmit} isSubmitting={isSubmitting} + onDelete={onDelete} + isDeleting={isDeleting} > void + isDeleting?: boolean + headerExtra?: ReactNode } export function FormModal({ @@ -16,17 +19,46 @@ export function FormModal({ children, submitLabel = 'Save', isSubmitting, + onDelete, + isDeleting, + headerExtra, }: FormModalProps) { + const [confirmingDelete, setConfirmingDelete] = useState(false) + + // Reset confirm state when modal closes/reopens + useEffect(() => { + setConfirmingDelete(false) + }, [onDelete]) + return (

{title}

+ {headerExtra}
{children}
-
+
+ {onDelete && ( + + )} +
- -
- ), - }, ] return ( @@ -123,6 +102,7 @@ export function AdminEvolutions() { isLoading={isLoading} emptyMessage="No evolutions found." keyFn={(e) => e.id} + onRowClick={(e) => setEditing(e)} /> {totalPages > 1 && ( @@ -189,19 +169,11 @@ export function AdminEvolutions() { } onClose={() => setEditing(null)} isSubmitting={updateEvolution.isPending} - /> - )} - - {deleting && ( - - deleteEvolution.mutate(deleting.id, { - onSuccess: () => setDeleting(null), + onDelete={() => + deleteEvolution.mutate(editing.id, { + onSuccess: () => setEditing(null), }) } - onCancel={() => setDeleting(null)} isDeleting={deleteEvolution.isPending} /> )} diff --git a/frontend/src/pages/admin/AdminGameDetail.tsx b/frontend/src/pages/admin/AdminGameDetail.tsx index 6cd115f..11fa2e0 100644 --- a/frontend/src/pages/admin/AdminGameDetail.tsx +++ b/frontend/src/pages/admin/AdminGameDetail.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { useParams, useNavigate, Link } from 'react-router-dom' +import { useParams, Link } from 'react-router-dom' import { DndContext, closestCenter, @@ -17,7 +17,6 @@ import { } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { RouteFormModal } from '../../components/admin/RouteFormModal' -import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal' import { BossBattleFormModal } from '../../components/admin/BossBattleFormModal' import { BossTeamEditor } from '../../components/admin/BossTeamEditor' import { useGame } from '../../hooks/useGames' @@ -39,13 +38,9 @@ import type { CreateBossBattleInput, UpdateBossBattleInput } from '../../types/a function SortableRouteRow({ route, - onEdit, - onDelete, onClick, }: { route: GameRoute - onEdit: (r: GameRoute) => void - onDelete: (r: GameRoute) => void onClick: (r: GameRoute) => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = @@ -83,29 +78,12 @@ function SortableRouteRow({ {route.order} {route.name} - -
e.stopPropagation()}> - - -
- ) } export function AdminGameDetail() { const { gameId } = useParams<{ gameId: string }>() - const navigate = useNavigate() const id = Number(gameId) const { data: game, isLoading } = useGame(id) @@ -121,10 +99,8 @@ export function AdminGameDetail() { const [tab, setTab] = useState<'routes' | 'bosses'>('routes') const [showCreate, setShowCreate] = useState(false) const [editing, setEditing] = useState(null) - const [deleting, setDeleting] = useState(null) const [showCreateBoss, setShowCreateBoss] = useState(false) const [editingBoss, setEditingBoss] = useState(null) - const [deletingBoss, setDeletingBoss] = useState(null) const [editingTeam, setEditingTeam] = useState(null) const sensors = useSensors( @@ -235,9 +211,6 @@ export function AdminGameDetail() { Name - - Actions - navigate(`/admin/games/${id}/routes/${r.id}`)} + onClick={(r) => setEditing(r)} /> ))} @@ -291,20 +262,13 @@ export function AdminGameDetail() { } onClose={() => setEditing(null)} isSubmitting={updateRoute.isPending} - /> - )} - - {deleting && ( - - deleteRoute.mutate(deleting.id, { - onSuccess: () => setDeleting(null), + onDelete={() => + deleteRoute.mutate(editing.id, { + onSuccess: () => setEditing(null), }) } - onCancel={() => setDeleting(null)} isDeleting={deleteRoute.isPending} + detailUrl={`/admin/games/${id}/routes/${editing.id}`} /> )} @@ -358,14 +322,15 @@ export function AdminGameDetail() { Team - - Actions - {bosses.map((boss) => ( - + setEditingBoss(boss)} + > {boss.order} {boss.name} @@ -374,28 +339,6 @@ export function AdminGameDetail() { {boss.location} {boss.levelCap} {boss.pokemon.length} - -
- - - -
- ))} @@ -434,20 +377,16 @@ export function AdminGameDetail() { } onClose={() => setEditingBoss(null)} isSubmitting={updateBoss.isPending} - /> - )} - - {deletingBoss && ( - - deleteBoss.mutate(deletingBoss.id, { - onSuccess: () => setDeletingBoss(null), + onDelete={() => + deleteBoss.mutate(editingBoss.id, { + onSuccess: () => setEditingBoss(null), }) } - onCancel={() => setDeletingBoss(null)} isDeleting={deleteBoss.isPending} + onEditTeam={() => { + setEditingTeam(editingBoss) + setEditingBoss(null) + }} /> )} diff --git a/frontend/src/pages/admin/AdminGames.tsx b/frontend/src/pages/admin/AdminGames.tsx index d15de9d..c1e6298 100644 --- a/frontend/src/pages/admin/AdminGames.tsx +++ b/frontend/src/pages/admin/AdminGames.tsx @@ -1,8 +1,6 @@ import { useState } from 'react' -import { useNavigate } from 'react-router-dom' import { AdminTable, type Column } from '../../components/admin/AdminTable' import { GameFormModal } from '../../components/admin/GameFormModal' -import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal' import { useGames } from '../../hooks/useGames' import { useCreateGame, useUpdateGame, useDeleteGame } from '../../hooks/useAdmin' import { exportGames } from '../../api/admin' @@ -10,7 +8,6 @@ import { downloadJson } from '../../utils/download' import type { Game, CreateGameInput, UpdateGameInput } from '../../types' export function AdminGames() { - const navigate = useNavigate() const { data: games = [], isLoading } = useGames() const createGame = useCreateGame() const updateGame = useUpdateGame() @@ -18,7 +15,6 @@ export function AdminGames() { const [showCreate, setShowCreate] = useState(false) const [editing, setEditing] = useState(null) - const [deleting, setDeleting] = useState(null) const columns: Column[] = [ { header: 'Name', accessor: (g) => g.name }, @@ -26,25 +22,6 @@ export function AdminGames() { { header: 'Region', accessor: (g) => g.region, sortKey: (g) => g.region }, { header: 'Gen', accessor: (g) => g.generation, sortKey: (g) => g.generation }, { header: 'Year', accessor: (g) => g.releaseYear ?? '-', sortKey: (g) => g.releaseYear ?? 0 }, - { - header: 'Actions', - accessor: (g) => ( -
e.stopPropagation()}> - - -
- ), - }, ] return ( @@ -75,7 +52,7 @@ export function AdminGames() { data={games} isLoading={isLoading} emptyMessage="No games yet. Add one to get started." - onRowClick={(g) => navigate(`/admin/games/${g.id}`)} + onRowClick={(g) => setEditing(g)} keyFn={(g) => g.id} /> @@ -102,20 +79,13 @@ export function AdminGames() { } onClose={() => setEditing(null)} isSubmitting={updateGame.isPending} - /> - )} - - {deleting && ( - - deleteGame.mutate(deleting.id, { - onSuccess: () => setDeleting(null), + onDelete={() => + deleteGame.mutate(editing.id, { + onSuccess: () => setEditing(null), }) } - onCancel={() => setDeleting(null)} isDeleting={deleteGame.isPending} + detailUrl={`/admin/games/${editing.id}`} /> )}
diff --git a/frontend/src/pages/admin/AdminPokemon.tsx b/frontend/src/pages/admin/AdminPokemon.tsx index 050acc8..da466f5 100644 --- a/frontend/src/pages/admin/AdminPokemon.tsx +++ b/frontend/src/pages/admin/AdminPokemon.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import { AdminTable, type Column } from '../../components/admin/AdminTable' import { PokemonFormModal } from '../../components/admin/PokemonFormModal' import { BulkImportModal } from '../../components/admin/BulkImportModal' -import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal' import { usePokemonList, useCreatePokemon, @@ -32,7 +31,6 @@ export function AdminPokemon() { const [showCreate, setShowCreate] = useState(false) const [showBulkImport, setShowBulkImport] = useState(false) const [editing, setEditing] = useState(null) - const [deleting, setDeleting] = useState(null) const columns: Column[] = [ { header: 'Dex #', accessor: (p) => p.nationalDex, className: 'w-16' }, @@ -48,25 +46,6 @@ export function AdminPokemon() { }, { header: 'Name', accessor: (p) => p.name }, { header: 'Types', accessor: (p) => p.types.join(', ') }, - { - header: 'Actions', - accessor: (p) => ( -
- - -
- ), - }, ] return ( @@ -120,6 +99,7 @@ export function AdminPokemon() { isLoading={isLoading} emptyMessage="No pokemon found." keyFn={(p) => p.id} + onRowClick={(p) => setEditing(p)} /> {/* Pagination */} @@ -194,19 +174,11 @@ export function AdminPokemon() { } onClose={() => setEditing(null)} isSubmitting={updatePokemon.isPending} - /> - )} - - {deleting && ( - - deletePokemon.mutate(deleting.id, { - onSuccess: () => setDeleting(null), + onDelete={() => + deletePokemon.mutate(editing.id, { + onSuccess: () => setEditing(null), }) } - onCancel={() => setDeleting(null)} isDeleting={deletePokemon.isPending} /> )} diff --git a/frontend/src/pages/admin/AdminRouteDetail.tsx b/frontend/src/pages/admin/AdminRouteDetail.tsx index b11d7b2..a3a2263 100644 --- a/frontend/src/pages/admin/AdminRouteDetail.tsx +++ b/frontend/src/pages/admin/AdminRouteDetail.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import { useParams, Link } from 'react-router-dom' import { AdminTable, type Column } from '../../components/admin/AdminTable' import { RouteEncounterFormModal } from '../../components/admin/RouteEncounterFormModal' -import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal' import { useGame, useRoutePokemon } from '../../hooks/useGames' import { useAddRouteEncounter, @@ -29,7 +28,6 @@ export function AdminRouteDetail() { const [showCreate, setShowCreate] = useState(false) const [editing, setEditing] = useState(null) - const [deleting, setDeleting] = useState(null) const route = game?.routes?.find((r) => r.id === rId) @@ -54,25 +52,6 @@ export function AdminRouteDetail() { accessor: (e) => e.minLevel === e.maxLevel ? `Lv ${e.minLevel}` : `Lv ${e.minLevel}-${e.maxLevel}`, }, - { - header: 'Actions', - accessor: (e) => ( -
- - -
- ), - }, ] return ( @@ -109,6 +88,7 @@ export function AdminRouteDetail() { isLoading={isLoading} emptyMessage="No pokemon assigned to this route yet." keyFn={(e) => e.id} + onRowClick={(e) => setEditing(e)} /> {showCreate && ( @@ -134,19 +114,11 @@ export function AdminRouteDetail() { } onClose={() => setEditing(null)} isSubmitting={updateEncounter.isPending} - /> - )} - - {deleting && ( - - removeEncounter.mutate(deleting.id, { - onSuccess: () => setDeleting(null), + onDelete={() => + removeEncounter.mutate(editing.id, { + onSuccess: () => setEditing(null), }) } - onCancel={() => setDeleting(null)} isDeleting={removeEncounter.isPending} /> )} diff --git a/frontend/src/pages/admin/AdminRuns.tsx b/frontend/src/pages/admin/AdminRuns.tsx index c9fc4ab..50512f3 100644 --- a/frontend/src/pages/admin/AdminRuns.tsx +++ b/frontend/src/pages/admin/AdminRuns.tsx @@ -46,19 +46,6 @@ export function AdminRuns() { accessor: (r) => new Date(r.startedAt).toLocaleDateString(), sortKey: (r) => r.startedAt, }, - { - header: 'Actions', - accessor: (r) => ( -
e.stopPropagation()}> - -
- ), - }, ] return ( @@ -73,6 +60,7 @@ export function AdminRuns() { isLoading={runsLoading || gamesLoading} emptyMessage="No runs yet." keyFn={(r) => r.id} + onRowClick={(r) => setDeleting(r)} /> {deleting && (