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:
@@ -7,6 +7,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
||||
### Geändert
|
||||
|
||||
- Kalender-Einstellungen: Das Feld „Adresse (URL)“ im Formular für Kalenderquellen zeigt jetzt je nach Typ ein passendes Beispiel (z. B. `https://mail.firma.de/EWS/Exchange.asmx` für Exchange EWS) und bei Exchange EWS einen Hinweis, dass die vollständige Adresse nötig ist – der Servername allein reicht nicht.
|
||||
- Kalender-Widget neu gestaltet: Monatsübersicht mit Terminanzahl je Tag (Termine beim Überfahren sichtbar) und darunter die nächsten Termine. In den Widget-Einstellungen lässt sich die Monatsansicht ein-/ausblenden sowie Anzahl und Zeitraum der angezeigten Termine wählen.
|
||||
|
||||
## 1.1.0 – 2026-09-16
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -73,7 +73,7 @@ Ihre Änderungen werden über **„Änderungen speichern"** übernommen. Verlass
|
||||
|---|---|
|
||||
| Uhr | Zeigt die aktuelle Uhrzeit an (optional mit Datum). Die Uhrzeit wächst und schrumpft mit der Kachel; wer eine feste Größe möchte, stellt sie unter Einstellungen > Dashboard als Schriftgröße in Punkt ein |
|
||||
| Suchleiste | Schnellsuche im Web über frei konfigurierbare Suchanbieter |
|
||||
| Kalender | Zeigt kommende Termine aus Ihren verbundenen Kalenderquellen |
|
||||
| Kalender | Monatsübersicht mit der Anzahl der Termine je Tag (die Termine eines Tages erscheinen, wenn Sie mit der Maus darüberfahren) und darunter die nächsten Termine aus Ihren verbundenen Kalenderquellen. Ob die Monatsansicht erscheint, wie viele Termine und welcher Zeitraum gezeigt werden, stellen Sie unter Einstellungen > Dashboard > Widgets ein |
|
||||
| Notizen | Freitext-Notizen mit Markdown-Formatierung |
|
||||
| Taschenrechner | Grundrechenarten, auch per Tastatur bedienbar |
|
||||
| Favoriten | Schnellzugriff auf mehrere selbst gepflegte Links, als Liste oder Kachelansicht |
|
||||
@@ -150,7 +150,7 @@ Ein einfaches Werkzeug, um zu prüfen, ob eine Internet-Domain verfügbar ist. G
|
||||
- **Akzentfarbe:** Passt die Hauptfarbe der Oberfläche an Ihren Geschmack an; über „Zurücksetzen" kehren Sie zur Standardfarbe zurück.
|
||||
- **Passwort ändern:** Nur sichtbar und nutzbar, wenn Ihr Konto **lokal** in Tessera verwaltet wird. Wird Ihr Konto stattdessen über das Verzeichnis (LDAP/Active Directory) verwaltet, zeigt Tessera stattdessen den Hinweis „Ihr Passwort wird über das Verzeichnis (LDAP) verwaltet. Eine Änderung ist hier nicht möglich." — in diesem Fall ändern Sie Ihr Passwort über die üblichen Firmenwege (z. B. Windows-Anmeldung), nicht in Tessera.
|
||||
|
||||
**Dashboard > Widgets:** Hier finden Sie für jedes auf Ihrem Dashboard platzierte Widget die zugehörigen Einstellungen, zum Beispiel eigene Suchanbieter für die Suchleiste.
|
||||
**Dashboard > Widgets:** Hier finden Sie für jedes auf Ihrem Dashboard platzierte Widget die zugehörigen Einstellungen, zum Beispiel eigene Suchanbieter für die Suchleiste oder beim Kalender die Monatsansicht (ein/aus), die Anzahl der angezeigten Termine (bis zu zehn, oder ausgeblendet) und den Zeitraum (7 bis 90 Tage).
|
||||
|
||||
**Dashboard > Kalender:** Hier verwalten Sie die Kalenderquellen, aus denen das Kalender-Widget seine Termine bezieht — Quellen hinzufügen, die Verbindung testen und nicht mehr benötigte Quellen wieder entfernen.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user