feat(web): CalendarConfig im Einstellungsfeld, Changelog- und Handbuch-Eintrag
- Kalender-Zweig im Einstellungsfeld ersetzt den englischen Fliesstext durch CalendarConfig (Monatsansicht ein/aus, Anzahl Termine 0..10, Zeitraum 7/14/30/60/90) nach Muster ClockConfig; Speichern per partiellem updateWidgetConfig
- Paneltest um drei Kalender-Tests erweitert (7/7 gruen), next-intl-Mock interpoliert jetzt {platzhalter}
- CHANGELOG.md: zweiter Eintrag unter Unveroeffentlicht/Geaendert; Anwenderhandbuch: Widget-Tabellenzeile und Dashboard-Widgets-Absatz ergaenzt
- Gesamtlauf: 51 Testdateien / 332 Tests gruen, Type-Check Exit 0 (quick-260916-htc)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,12 @@ vi.mock('next-intl', async () => {
|
||||
| string
|
||||
| undefined;
|
||||
return {
|
||||
useTranslations: (ns?: string) => (key: string) => lookup(ns ? `${ns}.${key}` : key) ?? key,
|
||||
useTranslations:
|
||||
(ns?: string) =>
|
||||
(key: string, values?: Record<string, unknown>) => {
|
||||
const raw = lookup(ns ? `${ns}.${key}` : key) ?? key;
|
||||
return values ? raw.replace(/\{(\w+)\}/g, (_: string, n: string) => String(values[n] ?? '')) : raw;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -36,6 +41,7 @@ vi.mock('@/components/settings/search-provider-form', () => ({
|
||||
}));
|
||||
|
||||
const clockTexts = (de as { widgets: { clock: Record<string, string> } }).widgets.clock;
|
||||
const cal = (de as { widgets: { calendar: Record<string, string> } }).widgets.calendar;
|
||||
|
||||
const WIDGET = { id: 'c1', widgetType: 'clock', config: { timezone: 'Europe/Berlin', timeFontSizePt: 24 } };
|
||||
|
||||
@@ -105,3 +111,74 @@ describe('WidgetSettingsPanel — Schriftgroesse der Uhrzeit (quick-260916-bwo)'
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WidgetSettingsPanel — Kalender-Einstellungen (quick-260916-htc)', () => {
|
||||
function renderCalendarExpanded(config: Record<string, unknown> = {}) {
|
||||
const onWidgetUpdate = vi.fn();
|
||||
const widget = { id: 'k1', widgetType: 'calendar', config };
|
||||
render(<WidgetSettingsPanel widgets={[widget]} onWidgetUpdate={onWidgetUpdate} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Kalender #1/ }));
|
||||
return { onWidgetUpdate };
|
||||
}
|
||||
|
||||
it('Test 5: Vorbelegung aus der Konfiguration, Optionslisten und uebersetzte Link-Zeile', () => {
|
||||
renderCalendarExpanded();
|
||||
|
||||
const showMonth = screen.getByLabelText(cal.configShowMonth) as HTMLInputElement;
|
||||
expect(showMonth.checked).toBe(true);
|
||||
|
||||
const maxEventsSelect = screen.getByLabelText(cal.configMaxEvents) as HTMLSelectElement;
|
||||
expect(maxEventsSelect.value).toBe('3');
|
||||
const maxEventsOptionTexts = Array.from(maxEventsSelect.options).map((o) => o.textContent);
|
||||
expect(maxEventsOptionTexts).toEqual([
|
||||
cal.configMaxEventsNone,
|
||||
cal.configMaxEventsOne,
|
||||
...Array.from({ length: 9 }, (_, i) => cal.configMaxEventsMany.replace('{count}', String(i + 2))),
|
||||
]);
|
||||
|
||||
const lookaheadSelect = screen.getByLabelText(cal.configLookahead) as HTMLSelectElement;
|
||||
expect(lookaheadSelect.value).toBe('30');
|
||||
const lookaheadOptionTexts = Array.from(lookaheadSelect.options).map((o) => o.textContent);
|
||||
expect(lookaheadOptionTexts).toEqual(
|
||||
[7, 14, 30, 60, 90].map((days) => cal.configLookaheadOption.replace('{days}', String(days))),
|
||||
);
|
||||
|
||||
expect(screen.getByText(cal.configSourcesHint)).toBeInTheDocument();
|
||||
const link = screen.getByRole('link', { name: cal.configSourcesLink });
|
||||
expect(link).toHaveAttribute('href', '/settings/dashboard/calendar');
|
||||
|
||||
expect(screen.queryByText(/managed under/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 6: jede Aenderung ruft updateWidgetConfig/onWidgetUpdate nur mit dem geaenderten Feld auf', async () => {
|
||||
const { onWidgetUpdate } = renderCalendarExpanded();
|
||||
|
||||
fireEvent.change(screen.getByLabelText(cal.configMaxEvents), { target: { value: '5' } });
|
||||
await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('k1', { maxEvents: 5 }));
|
||||
expect(updateWidgetConfig).toHaveBeenCalledWith('k1', { maxEvents: 5 });
|
||||
|
||||
fireEvent.change(screen.getByLabelText(cal.configLookahead), { target: { value: '14' } });
|
||||
await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('k1', { lookaheadDays: 14 }));
|
||||
expect(updateWidgetConfig).toHaveBeenCalledWith('k1', { lookaheadDays: 14 });
|
||||
|
||||
fireEvent.click(screen.getByLabelText(cal.configShowMonth));
|
||||
await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('k1', { showMonth: false }));
|
||||
expect(updateWidgetConfig).toHaveBeenCalledWith('k1', { showMonth: false });
|
||||
|
||||
expect(updateWidgetConfig).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('Test 7: bestehende Werte werden angezeigt, ungueltige Werte geklemmt', () => {
|
||||
renderCalendarExpanded({ showMonth: false, maxEvents: 7, lookaheadDays: 60 });
|
||||
|
||||
expect((screen.getByLabelText(cal.configShowMonth) as HTMLInputElement).checked).toBe(false);
|
||||
expect((screen.getByLabelText(cal.configMaxEvents) as HTMLSelectElement).value).toBe('7');
|
||||
expect((screen.getByLabelText(cal.configLookahead) as HTMLSelectElement).value).toBe('60');
|
||||
|
||||
cleanup();
|
||||
|
||||
renderCalendarExpanded({ maxEvents: 42, lookaheadDays: 45 });
|
||||
expect((screen.getByLabelText(cal.configMaxEvents) as HTMLSelectElement).value).toBe('10');
|
||||
expect((screen.getByLabelText(cal.configLookahead) as HTMLSelectElement).value).toBe('30');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,11 @@ import {
|
||||
CLOCK_FONT_SIZE_MIN_PT,
|
||||
resolveClockTimeFontSizePt,
|
||||
} from '@/components/dashboard/widgets/clock-font-size';
|
||||
import {
|
||||
CALENDAR_LOOKAHEAD_OPTIONS,
|
||||
CALENDAR_MAX_EVENTS_LIMIT,
|
||||
resolveCalendarConfig,
|
||||
} from '@/components/dashboard/widgets/calendar-month';
|
||||
|
||||
/**
|
||||
* IANA timezones offered for clock configuration (D-12).
|
||||
@@ -170,20 +175,14 @@ export function WidgetSettingsPanel({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Calendar config — managed elsewhere */}
|
||||
{/* Calendar config (quick-260916-htc) */}
|
||||
{widget.widgetType === 'calendar' && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>
|
||||
Calendar sources are managed under{' '}
|
||||
<Link
|
||||
href="/settings/dashboard/calendar"
|
||||
className="text-primary underline hover:text-primary/90"
|
||||
>
|
||||
Settings > Dashboard > Calendar
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<CalendarConfig
|
||||
config={widget.config}
|
||||
onChange={(cfg) =>
|
||||
handleConfigChange(widget.id, cfg)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -317,6 +316,90 @@ function ClockConfig({
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarConfig({
|
||||
config,
|
||||
onChange,
|
||||
}: {
|
||||
config: Record<string, unknown>;
|
||||
onChange: (cfg: Record<string, unknown>) => void;
|
||||
}) {
|
||||
const t = useTranslations('widgets');
|
||||
const { showMonth, maxEvents, lookaheadDays } = resolveCalendarConfig(config);
|
||||
|
||||
const maxEventsOptions = Array.from({ length: CALENDAR_MAX_EVENTS_LIMIT + 1 }, (_, n) => n);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Monatsansicht ein-/ausblenden */}
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
id="calendar-show-month"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-border text-primary"
|
||||
checked={showMonth}
|
||||
onChange={(e) => onChange({ showMonth: e.target.checked })}
|
||||
/>
|
||||
<label htmlFor="calendar-show-month" className="text-sm text-foreground">
|
||||
{t('calendar.configShowMonth')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Anzahl der angezeigten Termine */}
|
||||
<div>
|
||||
<label htmlFor="calendar-max-events" className="mb-1 block text-sm text-foreground">
|
||||
{t('calendar.configMaxEvents')}
|
||||
</label>
|
||||
<select
|
||||
id="calendar-max-events"
|
||||
className="h-9 w-full max-w-xs rounded border border-border bg-background px-3 text-sm text-foreground"
|
||||
value={String(maxEvents)}
|
||||
onChange={(e) => onChange({ maxEvents: Number(e.target.value) })}
|
||||
>
|
||||
{maxEventsOptions.map((n) => (
|
||||
<option key={n} value={String(n)}>
|
||||
{n === 0
|
||||
? t('calendar.configMaxEventsNone')
|
||||
: n === 1
|
||||
? t('calendar.configMaxEventsOne')
|
||||
: t('calendar.configMaxEventsMany', { count: n })}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Zeitraum der angezeigten Termine */}
|
||||
<div>
|
||||
<label htmlFor="calendar-lookahead" className="mb-1 block text-sm text-foreground">
|
||||
{t('calendar.configLookahead')}
|
||||
</label>
|
||||
<select
|
||||
id="calendar-lookahead"
|
||||
className="h-9 w-full max-w-xs rounded border border-border bg-background px-3 text-sm text-foreground"
|
||||
value={String(lookaheadDays)}
|
||||
onChange={(e) => onChange({ lookaheadDays: Number(e.target.value) })}
|
||||
>
|
||||
{CALENDAR_LOOKAHEAD_OPTIONS.map((days) => (
|
||||
<option key={days} value={String(days)}>
|
||||
{t('calendar.configLookaheadOption', { days })}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Hinweis auf die Kalenderquellenverwaltung */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('calendar.configSourcesHint')}{' '}
|
||||
<Link
|
||||
href="/settings/dashboard/calendar"
|
||||
className="text-primary underline hover:text-primary/90"
|
||||
>
|
||||
{t('calendar.configSourcesLink')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteConfig({
|
||||
config,
|
||||
onChange,
|
||||
|
||||
Reference in New Issue
Block a user