feat(quick-260916-dyv): Bearbeiten-Schalter unten rechts (Rand oben 28 px), ganze Kachel als Griff mit Kopfleiste, cancel-Selektor, kein Ueberlappen beim Ablegen
- page.tsx: feste Aktionsleiste `fixed bottom-6 right-6 z-20` mit "Widget
hinzufuegen" (nur Bearbeitungsmodus) links neben dem Stift/Haekchen; Block
oben rechts und mt-8-Wrapper entfernt, Grid direkt im Container p-2
(12 + 8 + 8 = 28 px statt 60 px); page.test.tsx NEU mit 3 Tests
- widget-wrapper.tsx: Karte ist im Bearbeitungsmodus der Griff (cursor-grab),
20-px-Overlay-Kopfleiste mit Griff-Symbol und Tooltip dragHint, Loesch-Knopf
in der Kopfleiste mit data-no-drag; Rumpf h-full unveraendert (Hoehenkette)
- dashboard-grid.tsx: WIDGET_DRAG_HANDLE_SELECTOR, WIDGET_DRAG_CANCEL_SELECTOR
(input, textarea, select, button, a, [contenteditable], [data-no-drag],
.widgetNoDrag), threshold 3; FREE_PLACEMENT_COMPACTOR = noCompactor +
preventCollision: true (freie Platzierung bleibt, Commit c8f3361)
- dashboard-grid.test.tsx: Mock per importOriginal (echter noCompactor),
Tests 6-8 (dragConfig-Pin, Compactor-Pin, cancel/handle-Semantik im DOM)
- edit-mode-toggle.tsx: schwebend (shadow-lg, inaktiv border + bg-card)
- de.json/en.json: widgets.dragHint
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// quick-260916-dyv: Dashboard-Seite — feste Aktionsleiste unten rechts
|
||||
// (Stift/Haekchen + "Widget hinzufuegen"), Grid direkt im Container p-2,
|
||||
// kein Abstands-Wrapper (mt-8) und kein Block oben rechts (top-2) mehr.
|
||||
// Muster: admin/groups/groups-page.test.tsx (next-intl-Mock mit Namensraum).
|
||||
|
||||
const messages: Record<string, Record<string, string>> = {
|
||||
widgets: {
|
||||
editMode: 'Dashboard bearbeiten',
|
||||
saveChanges: 'Änderungen speichern',
|
||||
addWidget: 'Widget hinzufügen',
|
||||
layoutLoadError: 'Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.',
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: (ns: string) => (key: string) => messages[ns]?.[key] ?? key,
|
||||
}));
|
||||
|
||||
const mockStore = {
|
||||
layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] },
|
||||
widgets: [] as Array<{ id: string; widgetType: string; config: Record<string, unknown> }>,
|
||||
isEditMode: false,
|
||||
isDirty: false,
|
||||
isLoading: false,
|
||||
error: null as string | null,
|
||||
setEditMode: vi.fn(),
|
||||
updateLayouts: vi.fn(),
|
||||
addWidget: vi.fn(),
|
||||
removeWidget: vi.fn(),
|
||||
loadDashboard: vi.fn(),
|
||||
saveLayout: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('@/lib/stores/dashboard-store', () => ({
|
||||
useDashboardStore: (selector?: (state: typeof mockStore) => unknown) => {
|
||||
if (selector) return selector(mockStore);
|
||||
return mockStore;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/components/dashboard/dashboard-grid', () => ({
|
||||
DashboardGrid: (p: { isEditMode: boolean }) => (
|
||||
<div data-testid="dashboard-grid" data-edit={String(p.isEditMode)} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/dashboard/widget-catalog-modal', () => ({
|
||||
WidgetCatalogModal: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/dashboard/widgets/clock-widget', () => ({ ClockWidget: () => null }));
|
||||
vi.mock('@/components/dashboard/widgets/search-widget', () => ({ SearchWidget: () => null }));
|
||||
vi.mock('@/components/dashboard/widgets/calendar-widget', () => ({ CalendarWidget: () => null }));
|
||||
vi.mock('@/components/dashboard/widgets/note-widget', () => ({ NoteWidget: () => null }));
|
||||
vi.mock('@/components/dashboard/widgets/calculator-widget', () => ({ CalculatorWidget: () => null }));
|
||||
vi.mock('@/components/dashboard/widgets/stopwatch-widget', () => ({ StopwatchWidget: () => null }));
|
||||
vi.mock('@/components/dashboard/widgets/favorites-widget', () => ({ FavoritesWidget: () => null }));
|
||||
vi.mock('@/components/dashboard/widgets/link-widget', () => ({ LinkWidget: () => null }));
|
||||
|
||||
beforeEach(() => {
|
||||
mockStore.isEditMode = false;
|
||||
mockStore.isLoading = false;
|
||||
mockStore.error = null;
|
||||
mockStore.setEditMode = vi.fn();
|
||||
mockStore.loadDashboard = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('DashboardPage (quick-260916-dyv)', () => {
|
||||
it('Test 1: Ansichtsmodus — Stift in fester Leiste unten rechts, kein "Widget hinzufuegen", kein mt-8/top-2, Grid direkt im Container p-2', async () => {
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: 'Dashboard bearbeiten' });
|
||||
const bar = toggle.parentElement as HTMLElement;
|
||||
expect(bar.classList.contains('fixed')).toBe(true);
|
||||
expect(bar.classList.contains('bottom-6')).toBe(true);
|
||||
expect(bar.classList.contains('right-6')).toBe(true);
|
||||
expect(bar.classList.contains('z-20')).toBe(true);
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Widget hinzufügen' })).toBeNull();
|
||||
expect(document.querySelector('.mt-8')).toBeNull();
|
||||
expect(document.querySelector('.top-2')).toBeNull();
|
||||
|
||||
const grid = screen.getByTestId('dashboard-grid');
|
||||
expect((grid.parentElement as HTMLElement).classList.contains('p-2')).toBe(true);
|
||||
expect(grid.getAttribute('data-edit')).toBe('false');
|
||||
});
|
||||
|
||||
it('Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben dem Haekchen in derselben Leiste, Grid im Bearbeitungsmodus', async () => {
|
||||
mockStore.isEditMode = true;
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: 'Änderungen speichern' });
|
||||
const add = screen.getByRole('button', { name: 'Widget hinzufügen' });
|
||||
expect(add.parentElement).toBe(toggle.parentElement);
|
||||
// "Widget hinzufuegen" steht im DOM VOR dem Umschalter (links daneben).
|
||||
expect(add.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
|
||||
expect(screen.getByTestId('dashboard-grid').getAttribute('data-edit')).toBe('true');
|
||||
});
|
||||
|
||||
it('Test 3: Umschalten — Klick auf "Dashboard bearbeiten" ruft setEditMode(true) genau einmal', async () => {
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard bearbeiten' }));
|
||||
expect(mockStore.setEditMode).toHaveBeenCalledTimes(1);
|
||||
expect(mockStore.setEditMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -67,32 +67,21 @@ export default function DashboardPage() {
|
||||
|
||||
return (
|
||||
<div className="relative p-2">
|
||||
{/* Edit mode toggle — top right */}
|
||||
<div className="absolute right-2 top-2 z-10">
|
||||
<EditModeToggle
|
||||
isEditMode={isEditMode}
|
||||
onToggle={() => setEditMode(!isEditMode)}
|
||||
/>
|
||||
</div>
|
||||
{/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */}
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={widgets}
|
||||
isEditMode={isEditMode}
|
||||
onLayoutChange={updateLayouts}
|
||||
onRemoveWidget={removeWidget}
|
||||
/>
|
||||
|
||||
{/* Dashboard grid.
|
||||
quick-260916-bwo: mt-8 bleibt bewusst — der Umschalter ist 36 px hoch
|
||||
(p-2 plus 20-px-Symbol) und liegt mit top-2 bei 8..44 px; das erste
|
||||
Widget beginnt mit mt-8 bei 8+32+8 = 48 px (4 px Abstand). Eine
|
||||
Halbierung auf mt-4 legte es bei 32 px mitten in den Umschalter. */}
|
||||
<div className="mt-8">
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={widgets}
|
||||
isEditMode={isEditMode}
|
||||
onLayoutChange={updateLayouts}
|
||||
onRemoveWidget={removeWidget}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add widget button (edit mode only) */}
|
||||
{isEditMode && (
|
||||
<div className="fixed bottom-6 right-6 z-20">
|
||||
{/* Feste Aktionsleiste unten rechts.
|
||||
quick-260916-dyv: Umschalter unten rechts statt oben rechts, damit das
|
||||
Grid direkt unter der Kopfzeile beginnt (12 + 8 + 8 = 28 px statt 60 px).
|
||||
Im Bearbeitungsmodus steht "Widget hinzufuegen" links neben dem Haekchen. */}
|
||||
<div className="fixed bottom-6 right-6 z-20 flex items-center gap-2">
|
||||
{isEditMode && (
|
||||
<button
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-lg hover:opacity-90 transition-opacity"
|
||||
@@ -113,8 +102,12 @@ export default function DashboardPage() {
|
||||
</svg>
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<EditModeToggle
|
||||
isEditMode={isEditMode}
|
||||
onToggle={() => setEditMode(!isEditMode)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Widget catalog modal */}
|
||||
<WidgetCatalogModal
|
||||
|
||||
Reference in New Issue
Block a user