mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 16:46:42 +02:00
feat(auth): add SessionExpiredBanner to all remaining web apps
Added to: clock, photos, storage, mukke, planta, picture, skilltree, nutriphi, chat. Now all 13 web apps show a re-login banner when token refresh permanently fails. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
90c438e267
commit
bf7517d24d
23 changed files with 842 additions and 19 deletions
|
|
@ -65,9 +65,29 @@ describe('parseEventInput', () => {
|
|||
it('should parse @calendar reference', () => {
|
||||
const result = parseEventInput('Meeting @Arbeit');
|
||||
expect(result.calendarName).toBe('Arbeit');
|
||||
expect(result.attendees).toEqual([]);
|
||||
expect(result.title).not.toContain('@Arbeit');
|
||||
});
|
||||
|
||||
it('should parse calendar and attendees from multiple @references', () => {
|
||||
const result = parseEventInput('Meeting @Arbeit @Max @Anna');
|
||||
expect(result.calendarName).toBe('Arbeit');
|
||||
expect(result.attendees).toEqual(['Max', 'Anna']);
|
||||
expect(result.title).not.toContain('@');
|
||||
});
|
||||
|
||||
it('should parse only attendees when no calendar match (first @ref treated as calendarName)', () => {
|
||||
const result = parseEventInput('Meeting @Max @Anna');
|
||||
expect(result.calendarName).toBe('Max');
|
||||
expect(result.attendees).toEqual(['Anna']);
|
||||
});
|
||||
|
||||
it('should parse single @reference as calendarName with no attendees', () => {
|
||||
const result = parseEventInput('Meeting @Arbeit');
|
||||
expect(result.calendarName).toBe('Arbeit');
|
||||
expect(result.attendees).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse #tags', () => {
|
||||
const result = parseEventInput('Meeting #wichtig #team');
|
||||
expect(result.tagNames).toEqual(['wichtig', 'team']);
|
||||
|
|
@ -109,10 +129,32 @@ describe('parseEventInput', () => {
|
|||
expect(result.endDate!.getHours()).toBe(17);
|
||||
});
|
||||
|
||||
it('should parse recurrence "jeden Montag"', () => {
|
||||
const result = parseEventInput('Standup jeden Montag 9 Uhr');
|
||||
expect(result.recurrenceRule).toBe('FREQ=WEEKLY;BYDAY=MO');
|
||||
expect(result.title).toBe('Standup');
|
||||
});
|
||||
|
||||
it('should parse recurrence "wöchentlich"', () => {
|
||||
const result = parseEventInput('Team-Meeting wöchentlich 14 Uhr');
|
||||
expect(result.recurrenceRule).toBe('FREQ=WEEKLY');
|
||||
});
|
||||
|
||||
it('should parse recurrence "täglich"', () => {
|
||||
const result = parseEventInput('Standup täglich');
|
||||
expect(result.recurrenceRule).toBe('FREQ=DAILY');
|
||||
});
|
||||
|
||||
it('should have no recurrence for normal input', () => {
|
||||
const result = parseEventInput('Meeting morgen');
|
||||
expect(result.recurrenceRule).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const result = parseEventInput('');
|
||||
expect(result.title).toBe('');
|
||||
expect(result.tagNames).toEqual([]);
|
||||
expect(result.attendees).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse time-only input (defaults to today)', () => {
|
||||
|
|
@ -151,10 +193,32 @@ describe('resolveEventIds', () => {
|
|||
expect(resolved.calendarId).toBe('cal-1');
|
||||
});
|
||||
|
||||
it('should skip unknown calendar', () => {
|
||||
it('should skip unknown calendar and treat it as attendee', () => {
|
||||
const parsed = parseEventInput('Meeting @Unbekannt');
|
||||
const resolved = resolveEventIds(parsed, calendars, tags);
|
||||
expect(resolved.calendarId).toBeUndefined();
|
||||
expect(resolved.attendees).toEqual(['Unbekannt']);
|
||||
});
|
||||
|
||||
it('should resolve calendar and keep attendees separate', () => {
|
||||
const parsed = parseEventInput('Meeting @Arbeit @Max @Anna');
|
||||
const resolved = resolveEventIds(parsed, calendars, tags);
|
||||
expect(resolved.calendarId).toBe('cal-1');
|
||||
expect(resolved.attendees).toEqual(['Max', 'Anna']);
|
||||
});
|
||||
|
||||
it('should treat all @refs as attendees when no calendar matches', () => {
|
||||
const parsed = parseEventInput('Meeting @Max @Anna');
|
||||
const resolved = resolveEventIds(parsed, calendars, tags);
|
||||
expect(resolved.calendarId).toBeUndefined();
|
||||
expect(resolved.attendees).toEqual(['Max', 'Anna']);
|
||||
});
|
||||
|
||||
it('should resolve calendar with no attendees', () => {
|
||||
const parsed = parseEventInput('Meeting @Arbeit');
|
||||
const resolved = resolveEventIds(parsed, calendars, tags);
|
||||
expect(resolved.calendarId).toBe('cal-1');
|
||||
expect(resolved.attendees).toEqual([]);
|
||||
});
|
||||
|
||||
it('should produce ISO date strings', () => {
|
||||
|
|
@ -180,6 +244,12 @@ describe('formatParsedEventPreview', () => {
|
|||
expect(preview).toContain('Arbeit');
|
||||
});
|
||||
|
||||
it('should format attendees', () => {
|
||||
const parsed = parseEventInput('Meeting @Arbeit @Max @Anna');
|
||||
const preview = formatParsedEventPreview(parsed);
|
||||
expect(preview).toContain('👥 Max, Anna');
|
||||
});
|
||||
|
||||
it('should format tags', () => {
|
||||
const parsed = parseEventInput('Meeting #team');
|
||||
const preview = formatParsedEventPreview(parsed);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
|
||||
import {
|
||||
parseBaseInput,
|
||||
extractAtReference,
|
||||
extractAtReferences,
|
||||
extractRecurrence,
|
||||
combineDateAndTime,
|
||||
formatDatePreview,
|
||||
formatTimePreview,
|
||||
|
|
@ -30,7 +31,9 @@ export interface ParsedEvent {
|
|||
duration?: number; // in minutes
|
||||
isAllDay?: boolean;
|
||||
calendarName?: string;
|
||||
attendees: string[];
|
||||
location?: string;
|
||||
recurrenceRule?: string;
|
||||
tagNames: string[];
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +53,9 @@ export interface ParsedEventWithIds {
|
|||
endTime?: string;
|
||||
isAllDay?: boolean;
|
||||
calendarId?: string;
|
||||
attendees: string[];
|
||||
location?: string;
|
||||
recurrenceRule?: string;
|
||||
tagIds: string[];
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +237,11 @@ function extractAllDay(
|
|||
export function parseEventInput(input: string, locale: ParserLocale = 'de'): ParsedEvent {
|
||||
let text = input.trim();
|
||||
|
||||
// Extract recurrence (before other extractions since "jeden Montag" could conflict with weekday)
|
||||
const recurrenceResult = extractRecurrence(text, locale);
|
||||
text = recurrenceResult.remaining;
|
||||
const recurrenceRule = recurrenceResult.value;
|
||||
|
||||
// Extract all-day flag
|
||||
const allDayResult = extractAllDay(text, locale);
|
||||
text = allDayResult.remaining;
|
||||
|
|
@ -246,10 +256,12 @@ export function parseEventInput(input: string, locale: ParserLocale = 'de'): Par
|
|||
text = durationResult.remaining;
|
||||
const duration = durationResult.duration;
|
||||
|
||||
// Extract calendar (@CalendarName)
|
||||
const calendarResult = extractAtReference(text);
|
||||
text = calendarResult.remaining;
|
||||
const calendarName = calendarResult.value;
|
||||
// Extract @references (first may be calendar, rest are attendees)
|
||||
const atRefsResult = extractAtReferences(text);
|
||||
text = atRefsResult.remaining;
|
||||
const atRefs = atRefsResult.value ?? [];
|
||||
const calendarName = atRefs.length > 0 ? atRefs[0] : undefined;
|
||||
const attendees = atRefs.length > 1 ? atRefs.slice(1) : [];
|
||||
|
||||
// Use base parser for common patterns (date, time, tags)
|
||||
const base = parseBaseInput(text, locale);
|
||||
|
|
@ -292,7 +304,9 @@ export function parseEventInput(input: string, locale: ParserLocale = 'de'): Par
|
|||
duration,
|
||||
isAllDay: isAllDay || undefined,
|
||||
calendarName,
|
||||
attendees,
|
||||
location,
|
||||
recurrenceRule,
|
||||
tagNames: base.tagNames,
|
||||
};
|
||||
}
|
||||
|
|
@ -311,15 +325,19 @@ export function resolveEventIds(
|
|||
defaultCalendarId?: string
|
||||
): ParsedEventWithIds {
|
||||
let calendarId: string | undefined;
|
||||
const attendees: string[] = [...parsed.attendees];
|
||||
const tagIds: string[] = [];
|
||||
|
||||
// Find calendar by name (case-insensitive)
|
||||
// Try to match first @ref as calendar (case-insensitive)
|
||||
if (parsed.calendarName) {
|
||||
const calendar = calendars.find(
|
||||
(c) => c.name.toLowerCase() === parsed.calendarName!.toLowerCase()
|
||||
);
|
||||
if (calendar) {
|
||||
calendarId = calendar.id;
|
||||
} else {
|
||||
// First @ref didn't match a calendar, treat it as an attendee
|
||||
attendees.unshift(parsed.calendarName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +360,9 @@ export function resolveEventIds(
|
|||
endTime: parsed.endDate?.toISOString(),
|
||||
isAllDay: parsed.isAllDay,
|
||||
calendarId,
|
||||
attendees,
|
||||
location: parsed.location,
|
||||
recurrenceRule: parsed.recurrenceRule,
|
||||
tagIds,
|
||||
};
|
||||
}
|
||||
|
|
@ -392,10 +412,18 @@ export function formatParsedEventPreview(parsed: ParsedEvent, locale: ParserLoca
|
|||
parts.push(`📍 ${parsed.location}`);
|
||||
}
|
||||
|
||||
if (parsed.recurrenceRule) {
|
||||
parts.push(`🔄 ${parsed.recurrenceRule}`);
|
||||
}
|
||||
|
||||
if (parsed.calendarName) {
|
||||
parts.push(`📆 ${parsed.calendarName}`);
|
||||
}
|
||||
|
||||
if (parsed.attendees.length > 0) {
|
||||
parts.push(`👥 ${parsed.attendees.join(', ')}`);
|
||||
}
|
||||
|
||||
if (parsed.tagNames.length > 0) {
|
||||
parts.push(`🏷️ ${parsed.tagNames.join(', ')}`);
|
||||
}
|
||||
|
|
|
|||
70
apps/calendar/apps/web/src/lib/utils/syntax-help.ts
Normal file
70
apps/calendar/apps/web/src/lib/utils/syntax-help.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Calendar-specific syntax help patterns for InputBar help modal
|
||||
*/
|
||||
import type { SyntaxGroup } from '@manacore/shared-ui';
|
||||
|
||||
export const CALENDAR_SYNTAX: SyntaxGroup[] = [
|
||||
{
|
||||
title: 'Kalender-Termin',
|
||||
items: [
|
||||
{
|
||||
pattern: 'Dauer',
|
||||
description: 'Termindauer angeben',
|
||||
examples: ['1h', '30min', '2h30m', '1 Stunde'],
|
||||
color: 'accent',
|
||||
},
|
||||
{
|
||||
pattern: 'Zeitbereich',
|
||||
description: 'Start- und Endzeit',
|
||||
examples: ['14-16 Uhr', '10:00-11:30', '9-17 Uhr'],
|
||||
color: 'accent',
|
||||
},
|
||||
{
|
||||
pattern: 'Ganztägig',
|
||||
description: 'Ganztägiger Termin',
|
||||
examples: ['ganztägig', 'ganzer Tag'],
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
pattern: 'Ort',
|
||||
description: 'Ort angeben',
|
||||
examples: ['in Berlin', 'im Büro', 'bei Dr. Müller'],
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
pattern: 'Wiederholung',
|
||||
description: 'Wiederkehrende Termine',
|
||||
examples: ['täglich', 'wöchentlich', 'jeden Montag', 'monatlich'],
|
||||
color: 'warning-soft',
|
||||
},
|
||||
{
|
||||
pattern: '@Kalender',
|
||||
description: 'Kalender zuweisen',
|
||||
examples: ['@Arbeit', '@Privat'],
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
pattern: '@Teilnehmer',
|
||||
description: 'Teilnehmer hinzufügen',
|
||||
examples: ['@Max', '@Anna'],
|
||||
color: 'success',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const CALENDAR_LIVE_EXAMPLE = {
|
||||
text: 'Teammeeting morgen 14-16 Uhr wöchentlich @Arbeit #wichtig',
|
||||
highlights: [
|
||||
{ type: 'text' as const, content: 'Teammeeting ' },
|
||||
{ type: 'date' as const, content: 'morgen' },
|
||||
{ type: 'text' as const, content: ' ' },
|
||||
{ type: 'time' as const, content: '14-16 Uhr' },
|
||||
{ type: 'text' as const, content: ' ' },
|
||||
{ type: 'date' as const, content: 'wöchentlich' },
|
||||
{ type: 'text' as const, content: ' ' },
|
||||
{ type: 'reference' as const, content: '@Arbeit' },
|
||||
{ type: 'text' as const, content: ' ' },
|
||||
{ type: 'tag' as const, content: '#wichtig' },
|
||||
],
|
||||
};
|
||||
|
|
@ -47,6 +47,7 @@
|
|||
resolveEventIds,
|
||||
formatParsedEventPreview,
|
||||
} from '$lib/utils/event-parser';
|
||||
import { CALENDAR_SYNTAX, CALENDAR_LIVE_EXAMPLE } from '$lib/utils/syntax-help';
|
||||
import UnifiedBar from '$lib/components/calendar/UnifiedBar.svelte';
|
||||
import SettingsModal from '$lib/components/settings/SettingsModal.svelte';
|
||||
import VoiceRecordButton from '$lib/components/voice/VoiceRecordButton.svelte';
|
||||
|
|
@ -581,7 +582,13 @@
|
|||
</SplitPaneContainer>
|
||||
|
||||
<!-- InputBar Help Modal -->
|
||||
<InputBarHelpModal open={helpModalOpen} onClose={handleCloseHelpModal} mode={helpModalMode} />
|
||||
<InputBarHelpModal
|
||||
open={helpModalOpen}
|
||||
onClose={handleCloseHelpModal}
|
||||
mode={helpModalMode}
|
||||
appSyntax={CALENDAR_SYNTAX}
|
||||
liveExample={CALENDAR_LIVE_EXAMPLE}
|
||||
/>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<SettingsModal visible={showSettingsModal} onClose={() => (showSettingsModal = false)} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue