mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
refactor(admin): share the admin shell actions between desktop and phone
Saving the demo baseline and the bag tracking and collab feature switches now live in useAdmin. AdminPage and the phone admin screen pass them on instead of carrying their own inline copies.
This commit is contained in:
@@ -24,7 +24,6 @@ import {
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { adminApi } from '../../../api/client'
|
||||
import { useTranslation } from '../../../i18n'
|
||||
import { useCountUp } from '../../../hooks/useCountUp'
|
||||
import { useAdmin } from '../../../pages/admin/useAdmin'
|
||||
@@ -71,12 +70,12 @@ export default function MAdmin() {
|
||||
const { t, locale } = useTranslation()
|
||||
const admin = useAdmin()
|
||||
const {
|
||||
demoMode, mcpEnabled, devMode, managed, toast, navigate,
|
||||
demoMode, mcpEnabled, devMode, managed, navigate,
|
||||
activeTab, setActiveTab, stats, serverTimezone,
|
||||
bagTrackingEnabled, setBagTrackingEnabled,
|
||||
collabFeatures, setCollabFeatures,
|
||||
bagTrackingEnabled, collabFeatures,
|
||||
setShowCreateUser,
|
||||
updateInfo, setShowUpdateModal,
|
||||
saveDemoBaseline, toggleBagTracking, toggleCollabFeature,
|
||||
} = admin
|
||||
const [sectionsOpen, setSectionsOpen] = useState(false)
|
||||
|
||||
@@ -101,15 +100,6 @@ export default function MAdmin() {
|
||||
]
|
||||
const activeSection = sections.find((s) => s.id === activeTab) ?? sections[0]
|
||||
|
||||
const saveDemoBaseline = async () => {
|
||||
try {
|
||||
await adminApi.saveDemoBaseline()
|
||||
toast.success('Baseline saved! Resets will restore to this state.')
|
||||
} catch (e) {
|
||||
toast.error(e.response?.data?.error || 'Failed to save baseline')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// Flow screen: scrolls with the document (#1809), so no height and no
|
||||
// scroll container of its own.
|
||||
@@ -221,27 +211,9 @@ export default function MAdmin() {
|
||||
{activeTab === 'addons' && (
|
||||
<MAdminAddonManager
|
||||
bagTrackingEnabled={bagTrackingEnabled}
|
||||
onToggleBagTracking={async () => {
|
||||
const next = !bagTrackingEnabled
|
||||
setBagTrackingEnabled(next)
|
||||
try {
|
||||
await adminApi.updateBagTracking(next)
|
||||
} catch {
|
||||
setBagTrackingEnabled(!next)
|
||||
}
|
||||
}}
|
||||
onToggleBagTracking={toggleBagTracking}
|
||||
collabFeatures={collabFeatures}
|
||||
onToggleCollabFeature={async (key: string) => {
|
||||
const previous = collabFeatures[key]
|
||||
setCollabFeatures({ ...collabFeatures, [key]: !previous })
|
||||
try {
|
||||
await adminApi.updateCollabFeatures({ [key]: !previous })
|
||||
} catch {
|
||||
// Only this key rolls back — a slower request must not undo a toggle
|
||||
// the admin made in the meantime.
|
||||
setCollabFeatures(prev => ({ ...prev, [key]: previous }))
|
||||
}
|
||||
}}
|
||||
onToggleCollabFeature={toggleCollabFeature}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'plugins' && <MAdminPluginsPanel />}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import SchoolHolidayCatalog from '../components/Admin/SchoolHolidayCatalog'
|
||||
import React, { Fragment } from 'react'
|
||||
import { adminApi } from '../api/client'
|
||||
import DevNotificationsPanel from '../components/Admin/DevNotificationsPanel'
|
||||
import DefaultUserSettingsTab from '../components/Admin/DefaultUserSettingsTab'
|
||||
import { useTranslation } from '../i18n'
|
||||
@@ -41,12 +40,12 @@ function AdminPageDesktop(): React.ReactElement {
|
||||
// each tab/section renders from a dedicated sub-component.
|
||||
const admin = useAdmin()
|
||||
const {
|
||||
demoMode, mcpEnabled, devMode, managed, toast,
|
||||
demoMode, mcpEnabled, devMode, managed,
|
||||
activeTab, setActiveTab, stats,
|
||||
bagTrackingEnabled, setBagTrackingEnabled,
|
||||
collabFeatures, setCollabFeatures,
|
||||
bagTrackingEnabled, collabFeatures,
|
||||
serverTimezone,
|
||||
updateInfo, setShowUpdateModal,
|
||||
saveDemoBaseline, toggleBagTracking, toggleCollabFeature,
|
||||
} = admin
|
||||
|
||||
const gUsers = t('admin.group.users')
|
||||
@@ -124,14 +123,7 @@ function AdminPageDesktop(): React.ReactElement {
|
||||
<p className="m-0 mt-0.5 text-content-muted" style={fs(12.5, 'body')}>Save current state as the hourly reset point. All admin trips and settings will be preserved.</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await adminApi.saveDemoBaseline()
|
||||
toast.success('Baseline saved! Resets will restore to this state.')
|
||||
} catch (e) {
|
||||
toast.error(e.response?.data?.error || 'Failed to save baseline')
|
||||
}
|
||||
}}
|
||||
onClick={saveDemoBaseline}
|
||||
className={`${SETTINGS_BUTTON_PRIMARY} flex-none`}
|
||||
style={fs(13, 'body')}
|
||||
>
|
||||
@@ -164,21 +156,7 @@ function AdminPageDesktop(): React.ReactElement {
|
||||
|
||||
{activeTab === 'addons' && (
|
||||
<div className="space-y-6">
|
||||
<AddonManager bagTrackingEnabled={bagTrackingEnabled} onToggleBagTracking={async () => {
|
||||
const next = !bagTrackingEnabled
|
||||
setBagTrackingEnabled(next)
|
||||
try { await adminApi.updateBagTracking(next) } catch { setBagTrackingEnabled(!next) }
|
||||
}} collabFeatures={collabFeatures} onToggleCollabFeature={async (key: string) => {
|
||||
const previous = collabFeatures[key]
|
||||
setCollabFeatures({ ...collabFeatures, [key]: !previous })
|
||||
try {
|
||||
await adminApi.updateCollabFeatures({ [key]: !previous })
|
||||
} catch {
|
||||
// Only this key rolls back — a slower request must not undo a toggle
|
||||
// the admin made in the meantime.
|
||||
setCollabFeatures(prev => ({ ...prev, [key]: previous }))
|
||||
}
|
||||
}} />
|
||||
<AddonManager bagTrackingEnabled={bagTrackingEnabled} onToggleBagTracking={toggleBagTracking} collabFeatures={collabFeatures} onToggleCollabFeature={toggleCollabFeature} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FE-ADMHOOK-001 to FE-ADMHOOK-053
|
||||
// FE-ADMHOOK-001 to FE-ADMHOOK-058
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import React from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -8,6 +8,7 @@ import { server } from '../../../tests/helpers/msw/server';
|
||||
import { act, renderHook, waitFor } from '../../../tests/helpers/render';
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import { TranslationProvider } from '../../i18n/TranslationContext';
|
||||
import { adminApi } from '../../api/client';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
import { useAdmin } from './useAdmin';
|
||||
@@ -863,4 +864,73 @@ describe('useAdmin', () => {
|
||||
expect(result.current.hour12).toBe(false);
|
||||
expect(result.current.currentUser?.username).toBe('admin');
|
||||
});
|
||||
it('FE-ADMHOOK-054: saving the demo baseline toasts the outcome, with the server message on failure', async () => {
|
||||
const save = vi.spyOn(adminApi, 'saveDemoBaseline').mockResolvedValue({});
|
||||
const { result } = await mountAdmin();
|
||||
|
||||
await act(() => result.current.saveDemoBaseline());
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
expect(toastCalls).toContainEqual({ type: 'success', message: 'Baseline saved! Resets will restore to this state.' });
|
||||
|
||||
save.mockRejectedValueOnce({ response: { data: { error: 'Not a demo' } } });
|
||||
await act(() => result.current.saveDemoBaseline());
|
||||
expect(toastCalls).toContainEqual({ type: 'error', message: 'Not a demo' });
|
||||
|
||||
save.mockRejectedValueOnce(new Error('offline'));
|
||||
await act(() => result.current.saveDemoBaseline());
|
||||
expect(toastCalls).toContainEqual({ type: 'error', message: 'Failed to save baseline' });
|
||||
});
|
||||
|
||||
it('FE-ADMHOOK-055: bag tracking flips at once and saves the new value', async () => {
|
||||
const update = vi.spyOn(adminApi, 'updateBagTracking').mockResolvedValue({});
|
||||
const { result } = await mountAdmin();
|
||||
const before = result.current.bagTrackingEnabled;
|
||||
|
||||
await act(() => result.current.toggleBagTracking());
|
||||
expect(update).toHaveBeenCalledWith(!before);
|
||||
expect(result.current.bagTrackingEnabled).toBe(!before);
|
||||
});
|
||||
|
||||
it('FE-ADMHOOK-056: a failed bag tracking save flips back without a toast', async () => {
|
||||
vi.spyOn(adminApi, 'updateBagTracking').mockRejectedValue(new Error('x'));
|
||||
const { result } = await mountAdmin();
|
||||
const before = result.current.bagTrackingEnabled;
|
||||
const toastsBefore = toastCalls.length;
|
||||
|
||||
await act(() => result.current.toggleBagTracking());
|
||||
expect(result.current.bagTrackingEnabled).toBe(before);
|
||||
expect(toastCalls).toHaveLength(toastsBefore);
|
||||
});
|
||||
|
||||
it('FE-ADMHOOK-057: a collab feature toggle saves only that key', async () => {
|
||||
vi.spyOn(adminApi, 'getCollabFeatures').mockResolvedValue({ chat: true, notes: true, polls: true, whatsnext: true });
|
||||
const update = vi.spyOn(adminApi, 'updateCollabFeatures').mockResolvedValue({});
|
||||
const { result } = await mountAdmin();
|
||||
await waitFor(() => expect(result.current.collabFeatures.chat).toBe(true));
|
||||
|
||||
await act(() => result.current.toggleCollabFeature('chat'));
|
||||
expect(update).toHaveBeenCalledWith({ chat: false });
|
||||
expect(result.current.collabFeatures).toEqual({ chat: false, notes: true, polls: true, whatsnext: true });
|
||||
});
|
||||
|
||||
it('FE-ADMHOOK-058: a failed collab toggle rolls back its own key and keeps a later one', async () => {
|
||||
vi.spyOn(adminApi, 'getCollabFeatures').mockResolvedValue({ chat: true, notes: true, polls: true, whatsnext: true });
|
||||
let fail!: (e: unknown) => void;
|
||||
vi.spyOn(adminApi, 'updateCollabFeatures')
|
||||
.mockReturnValueOnce(new Promise((_, reject) => (fail = reject)))
|
||||
.mockResolvedValueOnce({});
|
||||
const { result } = await mountAdmin();
|
||||
await waitFor(() => expect(result.current.collabFeatures.chat).toBe(true));
|
||||
|
||||
let pending!: Promise<void>;
|
||||
act(() => {
|
||||
pending = result.current.toggleCollabFeature('chat');
|
||||
});
|
||||
await act(() => result.current.toggleCollabFeature('notes'));
|
||||
await act(async () => {
|
||||
fail(new Error('x'));
|
||||
await pending;
|
||||
});
|
||||
expect(result.current.collabFeatures).toEqual({ chat: true, notes: false, polls: true, whatsnext: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -509,6 +509,38 @@ export function useAdmin() {
|
||||
}
|
||||
}
|
||||
|
||||
// The admin shell's own actions, the same on desktop and phone.
|
||||
const saveDemoBaseline = async () => {
|
||||
try {
|
||||
await adminApi.saveDemoBaseline()
|
||||
toast.success('Baseline saved! Resets will restore to this state.')
|
||||
} catch (e: unknown) {
|
||||
toast.error((e as { response?: { data?: { error?: string } } }).response?.data?.error || 'Failed to save baseline')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleBagTracking = async () => {
|
||||
const next = !bagTrackingEnabled
|
||||
setBagTrackingEnabled(next)
|
||||
try {
|
||||
await adminApi.updateBagTracking(next)
|
||||
} catch {
|
||||
setBagTrackingEnabled(!next)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCollabFeature = async (key: string) => {
|
||||
const previous = (collabFeatures as Record<string, boolean>)[key]
|
||||
setCollabFeatures({ ...collabFeatures, [key]: !previous })
|
||||
try {
|
||||
await adminApi.updateCollabFeatures({ [key]: !previous })
|
||||
} catch {
|
||||
// Only this key rolls back: a slower request must not undo a toggle the
|
||||
// admin made in the meantime.
|
||||
setCollabFeatures(prev => ({ ...prev, [key]: previous }))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// store-derived
|
||||
demoMode, serverTimezone, hour12, mcpEnabled, devMode, managed, currentUser,
|
||||
@@ -550,5 +582,6 @@ export function useAdmin() {
|
||||
toggleKey, handleSaveApiKeys, handleValidateKeys, handleValidateKey,
|
||||
handleCreateUser, handleCreateInvite, handleDeleteInvite, copyInviteLink,
|
||||
handleEditUser, handleSaveUser, handleDeleteUser,
|
||||
saveDemoBaseline, toggleBagTracking, toggleCollabFeature,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user