Fix dev bulk-delete button gate + reposition inside campaigns

Gate was process.env.NODE_ENV === 'development' — unsafe default. react-scripts
inlines NODE_ENV=development when unset, so prod deploys forgetting the env var
exposed the delete-all button. Switched to explicit opt-in REACT_APP_DEV_TOOLS=1.

process.env.REACT_APP_DEV_TOOLS as static literal gets inlined by DefinePlugin at
webpack build time — runtime mutations in tests had no effect, and dev-start
without the env produced bundles with the branch dead-stripped. Extracted gate
to src/config/devTools.js using dynamic key access
(process.env['REACT_APP_' + 'DEV_TOOLS']) so DefinePlugin cannot inline it; the
value is read at runtime. dev-start.sh now exports REACT_APP_DEV_TOOLS=1.

Button had also drifted outside the campaigns collapse block to the page bottom;
moved it back inside the campaigns section after the grid.

Tests cover both paths: gate logic (unset/0/arbitrary/1) in BulkDelete.gate.test.js,
prod safety render (button absent when unset) in BulkDelete.render-hidden.test.js,
dev feature render (button present when DEV_TOOLS=1) in BulkDelete.render-shown.test.js.
This commit is contained in:
david raistrick
2026-07-08 11:46:56 -04:00
parent 6f11edab94
commit 4a92c667c5
7 changed files with 139 additions and 12 deletions
+13 -12
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef, useMemo, createContext, useContext, useCallback } from 'react';
import * as shared from '@ttrpg/shared';
import { initializeApp, getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, getFirestore, where, orderBy, limit, offset, getStorage, getStorageMode } from './storage';
import { isDevToolsEnabled } from './config/devTools';
import {
PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown,
UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle,
@@ -2754,6 +2755,18 @@ function AdminView({ userId }) {
);
})}
</div>
{isDevToolsEnabled() && campaignsWithDetails.length > 0 && (
<div className="mt-4">
<button
onClick={() => setShowDeleteAllConfirm(true)}
className="px-3 py-1.5 text-xs rounded bg-red-900 hover:bg-red-800 text-red-100 border border-red-700"
title="DEV ONLY: Delete every campaign, encounter, and log. Irreversible."
>
<Trash2 size={12} className="inline mr-1" />DEV: Delete All Campaigns
</button>
</div>
)}
</>
)}
</div>
@@ -2800,18 +2813,6 @@ function AdminView({ userId }) {
message={`Are you sure you want to delete the campaign "${itemToDelete?.name}" and all its encounters? This action cannot be undone.`}
/>
{process.env.NODE_ENV === 'development' && campaignsWithDetails.length > 0 && (
<div className="mt-4">
<button
onClick={() => setShowDeleteAllConfirm(true)}
className="px-3 py-1.5 text-xs rounded bg-red-900 hover:bg-red-800 text-red-100 border border-red-700"
title="DEV ONLY: Delete every campaign, encounter, and log. Irreversible."
>
<Trash2 size={12} className="inline mr-1" />DEV: Delete All Campaigns
</button>
</div>
)}
<ConfirmationModal
isOpen={showDeleteAllConfirm}
onClose={() => setShowDeleteAllConfirm(false)}