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:
@@ -5,6 +5,23 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md.
|
||||
## Open
|
||||
|
||||
|
||||
fullscreen and dont lock on main app dm view and the no-game-player view
|
||||
|
||||
also better vert tab layout - labelt friendly
|
||||
|
||||
needs AC for players dude
|
||||
|
||||
and quick entry hp
|
||||
|
||||
hp do not carry from encounter to ecnounter!!!
|
||||
|
||||
|
||||
hp wont go over max and no temp hp support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### dm list - keep active particpant in view (scroll)
|
||||
not sure good way to do this
|
||||
|
||||
@@ -28,6 +28,7 @@ fi
|
||||
# frontend: server storage, :3999
|
||||
if ! lsof -ti :3999 >/dev/null 2>&1; then
|
||||
echo "starting frontend :3999..."
|
||||
NODE_ENV=development REACT_APP_DEV_TOOLS=1 \
|
||||
REACT_APP_STORAGE=server \
|
||||
REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \
|
||||
REACT_APP_BACKEND_REALTIME_URL=ws://127.0.0.1:4001/ws \
|
||||
|
||||
+13
-12
@@ -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)}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Dev-tools gate. Explicit opt-in via REACT_APP_DEV_TOOLS=1.
|
||||
// Safe default: any value other than exactly '1' = off.
|
||||
// Dynamic key access so react-scripts DefinePlugin does NOT inline the value
|
||||
// at build time — allows tests + runtime env changes to take effect.
|
||||
const KEY = 'REACT_APP_' + 'DEV_TOOLS';
|
||||
export const isDevToolsEnabled = () => process.env[KEY] === '1';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { isDevToolsEnabled } from '../config/devTools';
|
||||
|
||||
describe('Bulk-delete-all dev-tools gate logic', () => {
|
||||
const orig = process.env.REACT_APP_DEV_TOOLS;
|
||||
|
||||
afterEach(() => {
|
||||
if (orig === undefined) delete process.env.REACT_APP_DEV_TOOLS;
|
||||
else process.env.REACT_APP_DEV_TOOLS = orig;
|
||||
});
|
||||
|
||||
test('false when unset (safe default)', () => {
|
||||
delete process.env.REACT_APP_DEV_TOOLS;
|
||||
expect(isDevToolsEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('false when 0', () => {
|
||||
process.env.REACT_APP_DEV_TOOLS = '0';
|
||||
expect(isDevToolsEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('false when arbitrary string', () => {
|
||||
process.env.REACT_APP_DEV_TOOLS = 'true';
|
||||
expect(isDevToolsEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('true only when exactly 1', () => {
|
||||
process.env.REACT_APP_DEV_TOOLS = '1';
|
||||
expect(isDevToolsEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, fireEvent, cleanup, act } from '@testing-library/react';
|
||||
import App from '../App';
|
||||
|
||||
describe('Bulk-delete-all button render gating', () => {
|
||||
const orig = process.env.REACT_APP_DEV_TOOLS;
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
if (orig === undefined) delete process.env.REACT_APP_DEV_TOOLS;
|
||||
else process.env.REACT_APP_DEV_TOOLS = orig;
|
||||
});
|
||||
|
||||
async function renderAndCreateCampaign() {
|
||||
window.history.replaceState({}, '', '/');
|
||||
global.alert = jest.fn();
|
||||
global.window.open = jest.fn();
|
||||
render(<App />);
|
||||
await waitFor(() => screen.getByRole('button', { name: /Create Campaign/i }));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Create Campaign/i }));
|
||||
});
|
||||
await waitFor(() => screen.getByLabelText(/Campaign Name/i));
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByLabelText(/Campaign Name/i), { target: { value: 'Gate Render Test' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Create$/i }));
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
});
|
||||
}
|
||||
|
||||
test('prod safety: button absent when DEV_TOOLS unset', async () => {
|
||||
delete process.env.REACT_APP_DEV_TOOLS;
|
||||
await renderAndCreateCampaign();
|
||||
expect(screen.queryByText(/Delete All Campaigns/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, fireEvent, cleanup, act } from '@testing-library/react';
|
||||
import App from '../App';
|
||||
|
||||
describe('Bulk-delete-all button dev feature', () => {
|
||||
const orig = process.env.REACT_APP_DEV_TOOLS;
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
if (orig === undefined) delete process.env.REACT_APP_DEV_TOOLS;
|
||||
else process.env.REACT_APP_DEV_TOOLS = orig;
|
||||
});
|
||||
|
||||
async function renderAndCreateCampaign() {
|
||||
window.history.replaceState({}, '', '/');
|
||||
global.alert = jest.fn();
|
||||
global.window.open = jest.fn();
|
||||
render(<App />);
|
||||
await waitFor(() => screen.getByRole('button', { name: /Create Campaign/i }));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Create Campaign/i }));
|
||||
});
|
||||
await waitFor(() => screen.getByLabelText(/Campaign Name/i));
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByLabelText(/Campaign Name/i), { target: { value: 'Gate Render Test' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Create$/i }));
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
});
|
||||
}
|
||||
|
||||
test('dev: button present when DEV_TOOLS=1 and campaigns exist', async () => {
|
||||
process.env.REACT_APP_DEV_TOOLS = '1';
|
||||
await renderAndCreateCampaign();
|
||||
await waitFor(() => screen.getByText(/Delete All Campaigns/i), { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user