mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(settings): confirmations in the shared dialog, token colours on About, scope picker in the new look
This commit is contained in:
@@ -19,6 +19,13 @@ beforeEach(() => {
|
||||
seedStore(useAuthStore, { user: buildUser({ role: 'admin' }), isAuthenticated: true });
|
||||
});
|
||||
|
||||
/** Answers the confirm dialog: its Delete is the last one in the document (portal). */
|
||||
async function confirmDelete(user: ReturnType<typeof userEvent.setup>) {
|
||||
await screen.findByText('Delete category? Places in this category will not be deleted.');
|
||||
const deletes = screen.getAllByRole('button', { name: 'Delete' });
|
||||
await user.click(deletes[deletes.length - 1]);
|
||||
}
|
||||
|
||||
describe('CategoryManager', () => {
|
||||
it('FE-COMP-CAT-001: renders without crashing', () => {
|
||||
render(<CategoryManager />);
|
||||
@@ -116,15 +123,15 @@ describe('CategoryManager', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
})
|
||||
);
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
render(<><ToastContainer /><CategoryManager /></>);
|
||||
await screen.findByText('Parks');
|
||||
// Delete button is icon-only (Trash2, no title) — find the second action button
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const actionBtns = buttons.filter(b => !b.textContent?.includes('New Category'));
|
||||
await user.click(actionBtns[1]);
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
// Nothing goes before the confirm dialog is answered
|
||||
expect(await screen.findByText('Delete category? Places in this category will not be deleted.')).toBeInTheDocument();
|
||||
expect(deleteCalled).toBe(false);
|
||||
await confirmDelete(user);
|
||||
await waitFor(() => expect(deleteCalled).toBe(true));
|
||||
vi.restoreAllMocks();
|
||||
await waitFor(() => expect(screen.queryByText('Parks')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-010: shows subtitle text', async () => {
|
||||
@@ -209,23 +216,23 @@ describe('CategoryManager', () => {
|
||||
expect(screen.getByDisplayValue('Parks')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-016: declining the delete confirm keeps the category', async () => {
|
||||
it('FE-COMP-CAT-016: cancelling the delete confirm keeps the category', async () => {
|
||||
const user = userEvent.setup();
|
||||
let deleteCalled = false;
|
||||
server.use(
|
||||
http.get('/api/categories', () => HttpResponse.json({ categories: [buildCategory({ id: 9, name: 'Parks' })] })),
|
||||
http.delete('/api/categories/9', () => { deleteCalled = true; return HttpResponse.json({ success: true }); }),
|
||||
);
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
render(<CategoryManager />);
|
||||
await screen.findByText('Parks');
|
||||
|
||||
const actionBtns = screen.getAllByRole('button').filter(b => !b.textContent?.includes('New Category'));
|
||||
await user.click(actionBtns[1]);
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
await screen.findByText('Delete category? Places in this category will not be deleted.');
|
||||
await user.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||
|
||||
expect(screen.queryByText('Delete category? Places in this category will not be deleted.')).not.toBeInTheDocument();
|
||||
expect(deleteCalled).toBe(false);
|
||||
expect(screen.getByText('Parks')).toBeInTheDocument();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-017: a failing delete toasts and keeps the row', async () => {
|
||||
@@ -234,16 +241,14 @@ describe('CategoryManager', () => {
|
||||
http.get('/api/categories', () => HttpResponse.json({ categories: [buildCategory({ id: 9, name: 'Parks' })] })),
|
||||
http.delete('/api/categories/9', () => HttpResponse.json({ error: 'category in use' }, { status: 409 })),
|
||||
);
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
render(<><ToastContainer /><CategoryManager /></>);
|
||||
await screen.findByText('Parks');
|
||||
|
||||
const actionBtns = screen.getAllByRole('button').filter(b => !b.textContent?.includes('New Category'));
|
||||
await user.click(actionBtns[1]);
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
await confirmDelete(user);
|
||||
|
||||
expect(await screen.findByText('category in use')).toBeInTheDocument();
|
||||
expect(screen.getByText('Parks')).toBeInTheDocument();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-018: picking an icon and a preset colour updates the live preview', async () => {
|
||||
@@ -293,7 +298,7 @@ describe('CategoryManager', () => {
|
||||
await user.click(screen.getByText('New Category'));
|
||||
expect(screen.getByPlaceholderText('Category name')).toHaveValue('');
|
||||
|
||||
const row = screen.getByText('Hotels').closest('.p-3') as HTMLElement;
|
||||
const row = screen.getByText('Hotels').closest('[data-category-row]') as HTMLElement;
|
||||
await user.click(within(row).getAllByRole('button')[0]);
|
||||
|
||||
// Only the inline edit form remains, pre-filled with the row's name
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useToast } from '../shared/Toast'
|
||||
import { Plus, Pencil, Trash2, Pipette, Tags, Loader2 } from 'lucide-react'
|
||||
import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
import ConfirmDialog from '../shared/ConfirmDialog'
|
||||
import { SettingRows, SettingsCard, SettingsHint, StatusPill, SETTINGS_BUTTON_PRIMARY, SETTINGS_ICON_BUTTON } from '../Settings/settingsKit'
|
||||
import { CATEGORY_ICON_MAP, ICON_LABELS, getCategoryIcon } from '../shared/categoryIcons'
|
||||
import { useTranslation } from '../../i18n'
|
||||
@@ -27,6 +28,8 @@ export default function CategoryManager() {
|
||||
const [form, setForm] = useState({ name: '', color: DEFAULT_COLOR, icon: 'MapPin' })
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
// The category whose delete waits for the answer in the confirm dialog.
|
||||
const [deleteId, setDeleteId] = useState(null)
|
||||
const colorInputRef = useRef(null)
|
||||
const toast = useToast()
|
||||
const { t } = useTranslation()
|
||||
@@ -87,7 +90,6 @@ export default function CategoryManager() {
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm(t('categories.confirm.delete'))) return
|
||||
try {
|
||||
await categoriesApi.delete(id)
|
||||
setCategories(prev => prev.filter(c => c.id !== id))
|
||||
@@ -236,7 +238,7 @@ export default function CategoryManager() {
|
||||
{categories.map(cat => {
|
||||
const Icon = getCategoryIcon(cat.icon)
|
||||
return (
|
||||
<div key={cat.id} className={`group flex items-center gap-3 p-3 ${editingId === cat.id ? 'bg-surface-secondary' : ''}`}>
|
||||
<div key={cat.id} data-category-row={cat.id} className={`group flex items-center gap-3 px-3.5 py-3 ${editingId === cat.id ? 'bg-surface-secondary' : ''}`}>
|
||||
<span className="grid h-9 w-9 flex-none place-items-center rounded-[10px]"
|
||||
style={{ backgroundColor: `${cat.color}20` }}>
|
||||
<Icon size={17} strokeWidth={1.8} color={cat.color} />
|
||||
@@ -255,7 +257,7 @@ export default function CategoryManager() {
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('common.delete')}>
|
||||
<button type="button" onClick={() => handleDelete(cat.id)} aria-label={t('common.delete')} className={`${SETTINGS_ICON_BUTTON} hover:!text-danger`}>
|
||||
<button type="button" onClick={() => setDeleteId(cat.id)} aria-label={t('common.delete')} className={`${SETTINGS_ICON_BUTTON} hover:!text-danger`}>
|
||||
<Trash2 size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
@@ -266,6 +268,14 @@ export default function CategoryManager() {
|
||||
</SettingRows>
|
||||
)}
|
||||
{formOpen && categoryDialog}
|
||||
<ConfirmDialog
|
||||
isOpen={deleteId !== null}
|
||||
onClose={() => setDeleteId(null)}
|
||||
onConfirm={() => { if (deleteId !== null) void handleDelete(deleteId) }}
|
||||
message={t('categories.confirm.delete')}
|
||||
confirmLabel={t('common.delete')}
|
||||
danger
|
||||
/>
|
||||
</SettingsCard>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user