feat(quick-260916-bwo): Dashboard-Raster verdoppelt (24 Spalten, 20 px, 8 px Abstand), Konstanten x2, einmalige Umrechnung gespeicherter Anordnungen mit Marker __gridVersion
- dashboard-grid.tsx: COLS 24/20/12/8/2, rowHeight 20, margin 8 (containerPadding folgt), Rueckfallwerte 4 - widget-registry.tsx: alle 32 Werte in WIDGET_CONSTRAINTS verdoppelt - grid-layout-migration.ts (neu): migrateGridLayouts/withGridVersion, Marker nur im JSON, Idempotenz (T-BWO-02) - dashboard-store.ts: Umrechnung beim Laden, Sofort-Speichern mit Marker, withGridVersion bei jedem saveLayout - Tests: Migration 7 (neu), Store 6 (neu), Registry +1 (Tabelle), Grid +2 (Props ueber Mock), API-Spec +2 (Durchreichung __gridVersion, timeFontSizePt) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Children, isValidElement } from 'react';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -34,13 +35,15 @@ vi.mock('next-intl', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock react-grid-layout Responsive to a simple passthrough for jsdom stability
|
||||
// Mock react-grid-layout Responsive to a simple passthrough for jsdom stability.
|
||||
// quick-260916-bwo: der Mock faengt zusaetzlich die Props ein, damit die
|
||||
// Raster-Konstanten (cols, rowHeight, margin, breakpoints) pruefbar sind.
|
||||
const captured = vi.hoisted(() => ({ props: null as Record<string, unknown> | null }));
|
||||
vi.mock('react-grid-layout', () => ({
|
||||
Responsive: ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => <div data-testid="responsive-grid">{children}</div>,
|
||||
Responsive: (props: Record<string, unknown> & { children: React.ReactNode }) => {
|
||||
captured.props = props;
|
||||
return <div data-testid="responsive-grid">{props.children}</div>;
|
||||
},
|
||||
noCompactor: () => null,
|
||||
}));
|
||||
|
||||
@@ -153,4 +156,46 @@ describe('DashboardGrid', () => {
|
||||
expect(screen.getByLabelText('Remove widget')).toBeInTheDocument();
|
||||
expect(document.querySelector('.widget-drag-handle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('quick-260916-bwo Test 4: Grid-Props — 24/20/12/8/2 Spalten, rowHeight 20, margin 8, Breakpoints unveraendert, containerPadding folgt dem margin', async () => {
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
render(
|
||||
<DashboardGrid
|
||||
layouts={{ lg: [{ i: 'inst-1', x: 0, y: 0, w: 4, h: 4 }], md: [], sm: [], xs: [], xxs: [] }}
|
||||
widgets={[{ id: 'inst-1', widgetType: 'clock', config: {} }]}
|
||||
isEditMode={false}
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(captured.props).not.toBeNull();
|
||||
expect(captured.props?.cols).toEqual({ lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 });
|
||||
expect(captured.props?.rowHeight).toBe(20);
|
||||
expect(captured.props?.margin).toEqual([8, 8]);
|
||||
expect(captured.props?.breakpoints).toEqual({ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 });
|
||||
expect(captured.props?.containerPadding).toBeUndefined();
|
||||
});
|
||||
|
||||
it('quick-260916-bwo Test 5: Widget ohne gespeicherten Eintrag bekommt die verdoppelten Vorgaben als data-grid', async () => {
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
render(
|
||||
<DashboardGrid
|
||||
layouts={{ lg: [], md: [], sm: [], xs: [], xxs: [] }}
|
||||
widgets={[{ id: 'inst-3', widgetType: 'clock', config: {} }]}
|
||||
isEditMode={false}
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const children = Children.toArray(captured.props?.children as React.ReactNode);
|
||||
expect(children).toHaveLength(1);
|
||||
const first = children[0];
|
||||
expect(isValidElement(first)).toBe(true);
|
||||
const props = (first as React.ReactElement<Record<string, unknown>>).props;
|
||||
expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 4, h: 4, minW: 4, minH: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,12 @@ import { WIDGET_CONSTRAINTS, type WidgetType } from './widget-registry';
|
||||
import { WidgetWrapper } from './widgets/widget-wrapper';
|
||||
import { TesseraLogo } from '@/components/brand/tessera-logo';
|
||||
|
||||
// quick-260916-bwo: Raster doppelt so fein wie zuvor (24 statt 12 Spalten,
|
||||
// 20 statt 40 px Zeilenhoehe, 8 statt 16 px Abstand). Gespeicherte Anordnungen
|
||||
// in alten Einheiten werden in `@/lib/grid-layout-migration` einmalig
|
||||
// umgerechnet und mit `__gridVersion: 2` markiert. BREAKPOINTS unveraendert.
|
||||
const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 };
|
||||
const COLS = { lg: 12, md: 10, sm: 6, xs: 4, xxs: 1 };
|
||||
const COLS = { lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 };
|
||||
|
||||
interface DashboardGridProps {
|
||||
layouts: Record<string, Array<{ i: string; x: number; y: number; w: number; h: number }>>;
|
||||
@@ -74,8 +78,10 @@ export function DashboardGrid({
|
||||
breakpoints={BREAKPOINTS}
|
||||
cols={COLS}
|
||||
layouts={layouts as ResponsiveLayouts}
|
||||
rowHeight={40}
|
||||
margin={[16, 16] as [number, number]}
|
||||
rowHeight={20}
|
||||
margin={[8, 8] as [number, number]}
|
||||
// containerPadding bewusst nicht gesetzt: react-grid-layout 2.2.3 nimmt
|
||||
// dann das margin (gemessen: effectiveContainerPadding = containerPadding ?? margin).
|
||||
dragConfig={{
|
||||
enabled: isEditMode,
|
||||
handle: '.widget-drag-handle',
|
||||
@@ -95,11 +101,11 @@ export function DashboardGrid({
|
||||
...(layouts.lg?.find((l) => l.i === widget.id) ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: constraints?.defaultW ?? 2,
|
||||
h: constraints?.defaultH ?? 2,
|
||||
w: constraints?.defaultW ?? 4,
|
||||
h: constraints?.defaultH ?? 4,
|
||||
}),
|
||||
minW: constraints?.minW ?? 2,
|
||||
minH: constraints?.minH ?? 2,
|
||||
minW: constraints?.minW ?? 4,
|
||||
minH: constraints?.minH ?? 4,
|
||||
}}
|
||||
>
|
||||
<WidgetWrapper
|
||||
|
||||
@@ -52,4 +52,28 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => {
|
||||
expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('link');
|
||||
expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('stopwatch');
|
||||
});
|
||||
|
||||
it('quick-260916-bwo: jede Groesse ist exakt das Doppelte der alten 12-Spalten-Werte', () => {
|
||||
// Raster seit quick-260916-bwo 24 Spalten / 20 px statt 12 / 40 — jeder Wert
|
||||
// ist das Doppelte, damit Widgets optisch gleich gross bleiben.
|
||||
expect(WIDGET_CONSTRAINTS).toEqual({
|
||||
clock: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
||||
search: { minW: 6, minH: 4, defaultW: 12, defaultH: 4 },
|
||||
calendar: { minW: 6, minH: 6, defaultW: 8, defaultH: 12 },
|
||||
note: { minW: 4, minH: 6, defaultW: 6, defaultH: 8 },
|
||||
calculator: { minW: 4, minH: 8, defaultW: 6, defaultH: 10 },
|
||||
favorites: { minW: 4, minH: 6, defaultW: 6, defaultH: 10 },
|
||||
link: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
||||
stopwatch: { minW: 4, minH: 4, defaultW: 6, defaultH: 6 },
|
||||
});
|
||||
|
||||
let counted = 0;
|
||||
for (const type of ALL_WIDGET_TYPES) {
|
||||
for (const field of ['minW', 'minH', 'defaultW', 'defaultH'] as const) {
|
||||
expect(WIDGET_CONSTRAINTS[type][field] % 2, `${type}.${field} muss gerade sein`).toBe(0);
|
||||
counted++;
|
||||
}
|
||||
}
|
||||
expect(counted).toBe(32);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,16 +32,17 @@ export const WIDGET_CONSTRAINTS: Record<
|
||||
WidgetType,
|
||||
{ minW: number; minH: number; defaultW: number; defaultH: number }
|
||||
> = {
|
||||
// Existing widgets (values unchanged)
|
||||
clock: { minW: 2, minH: 2, defaultW: 2, defaultH: 2 },
|
||||
search: { minW: 3, minH: 2, defaultW: 6, defaultH: 2 },
|
||||
calendar: { minW: 3, minH: 3, defaultW: 4, defaultH: 6 },
|
||||
note: { minW: 2, minH: 3, defaultW: 3, defaultH: 4 },
|
||||
// quick-260916-bwo: Raster verdoppelt (24 Spalten / 20 px) — jeder Wert ist das Doppelte des alten 12-Spalten-Werts, Widgets bleiben optisch gleich gross.
|
||||
// Existing widgets
|
||||
clock: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
||||
search: { minW: 6, minH: 4, defaultW: 12, defaultH: 4 },
|
||||
calendar: { minW: 6, minH: 6, defaultW: 8, defaultH: 12 },
|
||||
note: { minW: 4, minH: 6, defaultW: 6, defaultH: 8 },
|
||||
// Phase 8 new widgets (D-01)
|
||||
calculator: { minW: 2, minH: 4, defaultW: 3, defaultH: 5 },
|
||||
favorites: { minW: 2, minH: 3, defaultW: 3, defaultH: 5 },
|
||||
link: { minW: 2, minH: 2, defaultW: 2, defaultH: 2 },
|
||||
stopwatch: { minW: 2, minH: 2, defaultW: 3, defaultH: 3 },
|
||||
calculator: { minW: 4, minH: 8, defaultW: 6, defaultH: 10 },
|
||||
favorites: { minW: 4, minH: 6, defaultW: 6, defaultH: 10 },
|
||||
link: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
||||
stopwatch: { minW: 4, minH: 4, defaultW: 6, defaultH: 6 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user