mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 03:46:41 +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
55
apps/todo/apps/web/src/lib/utils/syntax-help.ts
Normal file
55
apps/todo/apps/web/src/lib/utils/syntax-help.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Todo-specific syntax help patterns for InputBar help modal
|
||||
*/
|
||||
import type { SyntaxGroup } from '@manacore/shared-ui';
|
||||
|
||||
export const TODO_SYNTAX: SyntaxGroup[] = [
|
||||
{
|
||||
title: 'Aufgaben',
|
||||
items: [
|
||||
{
|
||||
pattern: 'Priorität',
|
||||
description: 'Dringlichkeit festlegen',
|
||||
examples: [
|
||||
{ text: '!!!', label: 'dringend', color: 'error' },
|
||||
{ text: '!!', label: 'hoch', color: 'warning' },
|
||||
{ text: 'normal', label: 'normal', color: 'warning-soft' },
|
||||
{ text: 'später', label: 'niedrig', color: 'success' },
|
||||
],
|
||||
color: 'error',
|
||||
},
|
||||
{
|
||||
pattern: '@Projekt',
|
||||
description: 'Projekt zuweisen',
|
||||
examples: ['@Arbeit', '@Privat', '@Einkauf'],
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
pattern: 'Wiederholung',
|
||||
description: 'Wiederkehrende Aufgabe',
|
||||
examples: ['täglich', 'wöchentlich', 'jeden Montag', 'monatlich'],
|
||||
color: 'warning-soft',
|
||||
},
|
||||
{
|
||||
pattern: 'Subtasks',
|
||||
description: 'Unteraufgaben mit Doppelpunkt + Komma',
|
||||
examples: ['Einkaufen: Milch, Brot, Eier'],
|
||||
color: 'accent',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const TODO_LIVE_EXAMPLE = {
|
||||
text: 'Einkaufen: Milch, Brot morgen !! @Privat #wichtig',
|
||||
highlights: [
|
||||
{ type: 'text' as const, content: 'Einkaufen: Milch, Brot ' },
|
||||
{ type: 'date' as const, content: 'morgen' },
|
||||
{ type: 'text' as const, content: ' ' },
|
||||
{ type: 'priority' as const, content: '!!' },
|
||||
{ type: 'text' as const, content: ' ' },
|
||||
{ type: 'reference' as const, content: '@Privat' },
|
||||
{ type: 'text' as const, content: ' ' },
|
||||
{ type: 'tag' as const, content: '#wichtig' },
|
||||
],
|
||||
};
|
||||
|
|
@ -73,6 +73,53 @@ describe('parseTaskInput', () => {
|
|||
const result = parseTaskInput('#arbeit #privat');
|
||||
expect(result.labelNames).toEqual(['arbeit', 'privat']);
|
||||
});
|
||||
|
||||
it('should parse recurrence "täglich"', () => {
|
||||
const result = parseTaskInput('Standup täglich 9 Uhr');
|
||||
expect(result.recurrenceRule).toBe('FREQ=DAILY');
|
||||
expect(result.title).toBe('Standup');
|
||||
});
|
||||
|
||||
it('should parse recurrence "jeden Montag"', () => {
|
||||
const result = parseTaskInput('Wochenbericht jeden Montag');
|
||||
expect(result.recurrenceRule).toBe('FREQ=WEEKLY;BYDAY=MO');
|
||||
});
|
||||
|
||||
it('should parse recurrence "wöchentlich"', () => {
|
||||
const result = parseTaskInput('Review wöchentlich @Arbeit');
|
||||
expect(result.recurrenceRule).toBe('FREQ=WEEKLY');
|
||||
expect(result.projectName).toBe('Arbeit');
|
||||
});
|
||||
|
||||
it('should have no recurrence for normal input', () => {
|
||||
const result = parseTaskInput('Einfache Aufgabe');
|
||||
expect(result.recurrenceRule).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should parse subtasks "Einkaufen: Milch, Brot, Eier"', () => {
|
||||
const result = parseTaskInput('Einkaufen: Milch, Brot, Eier');
|
||||
expect(result.title).toBe('Einkaufen');
|
||||
expect(result.subtasks).toEqual(['Milch', 'Brot', 'Eier']);
|
||||
});
|
||||
|
||||
it('should parse subtasks with semicolons', () => {
|
||||
const result = parseTaskInput('Aufräumen: Küche; Bad; Wohnzimmer');
|
||||
expect(result.title).toBe('Aufräumen');
|
||||
expect(result.subtasks).toEqual(['Küche', 'Bad', 'Wohnzimmer']);
|
||||
});
|
||||
|
||||
it('should not parse subtasks with single item', () => {
|
||||
const result = parseTaskInput('Note: important thing');
|
||||
expect(result.subtasks).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should parse subtasks with other fields', () => {
|
||||
const result = parseTaskInput('Einkaufen: Milch, Brot morgen !! @Privat');
|
||||
expect(result.title).toBe('Einkaufen');
|
||||
expect(result.subtasks).toEqual(['Milch', 'Brot']);
|
||||
expect(result.priority).toBe('high');
|
||||
expect(result.projectName).toBe('Privat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTaskIds', () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import {
|
||||
parseBaseInput,
|
||||
extractAtReference,
|
||||
extractRecurrence,
|
||||
combineDateAndTime,
|
||||
formatDatePreview,
|
||||
formatTimePreview,
|
||||
|
|
@ -22,6 +23,8 @@ export interface ParsedTask {
|
|||
priority?: TaskPriority;
|
||||
projectName?: string;
|
||||
labelNames: string[];
|
||||
recurrenceRule?: string;
|
||||
subtasks?: string[];
|
||||
}
|
||||
|
||||
interface Project {
|
||||
|
|
@ -40,6 +43,8 @@ export interface ParsedTaskWithIds {
|
|||
priority?: TaskPriority;
|
||||
projectId?: string;
|
||||
labelIds: string[];
|
||||
recurrenceRule?: string;
|
||||
subtasks?: string[];
|
||||
}
|
||||
|
||||
// Priority keyword translations per locale
|
||||
|
|
@ -54,6 +59,31 @@ const PRIORITY_KEYWORDS: Record<
|
|||
it: { urgent: 'urgente', high: 'importante', medium: 'normale', low: 'dopo' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract subtasks from "Title: item1, item2, item3" pattern
|
||||
*/
|
||||
function extractSubtasks(text: string): { title: string; subtasks?: string[] } {
|
||||
// Match "Title: list" where list has commas or semicolons
|
||||
const colonIndex = text.indexOf(':');
|
||||
if (colonIndex === -1 || colonIndex < 2) return { title: text };
|
||||
|
||||
const beforeColon = text.substring(0, colonIndex).trim();
|
||||
const afterColon = text.substring(colonIndex + 1).trim();
|
||||
|
||||
if (!afterColon) return { title: text };
|
||||
|
||||
// Split by comma or semicolon
|
||||
const items = afterColon
|
||||
.split(/[,;]/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
// Only treat as subtasks if there are at least 2 items
|
||||
if (items.length < 2) return { title: text };
|
||||
|
||||
return { title: beforeColon, subtasks: items };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build locale-aware priority patterns
|
||||
*/
|
||||
|
|
@ -99,7 +129,12 @@ function extractPriority(
|
|||
export function parseTaskInput(input: string, locale: ParserLocale = 'de'): ParsedTask {
|
||||
let text = input.trim();
|
||||
|
||||
// Extract priority first (task-specific)
|
||||
// Extract recurrence (before priority, since "jeden Tag" shouldn't be confused)
|
||||
const recurrenceResult = extractRecurrence(text, locale);
|
||||
text = recurrenceResult.remaining;
|
||||
const recurrenceRule = recurrenceResult.value;
|
||||
|
||||
// Extract priority (task-specific)
|
||||
const priorityResult = extractPriority(text, locale);
|
||||
text = priorityResult.remaining;
|
||||
const priority = priorityResult.priority;
|
||||
|
|
@ -115,12 +150,17 @@ export function parseTaskInput(input: string, locale: ParserLocale = 'de'): Pars
|
|||
// Combine date and time
|
||||
const dueDate = combineDateAndTime(base.date, base.time);
|
||||
|
||||
// Check for subtask pattern "Title: item1, item2, item3"
|
||||
const subtaskResult = extractSubtasks(base.title);
|
||||
|
||||
return {
|
||||
title: base.title,
|
||||
title: subtaskResult.title,
|
||||
dueDate,
|
||||
priority,
|
||||
projectName,
|
||||
labelNames: base.tagNames,
|
||||
recurrenceRule,
|
||||
subtasks: subtaskResult.subtasks,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +199,8 @@ export function resolveTaskIds(
|
|||
priority: parsed.priority,
|
||||
projectId,
|
||||
labelIds,
|
||||
recurrenceRule: parsed.recurrenceRule,
|
||||
subtasks: parsed.subtasks,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +241,14 @@ export function formatParsedTaskPreview(parsed: ParsedTask, locale: ParserLocale
|
|||
parts.push(`📁 ${parsed.projectName}`);
|
||||
}
|
||||
|
||||
if (parsed.recurrenceRule) {
|
||||
parts.push(`🔄 ${parsed.recurrenceRule}`);
|
||||
}
|
||||
|
||||
if (parsed.subtasks && parsed.subtasks.length > 0) {
|
||||
parts.push(`📋 ${parsed.subtasks.length} Subtasks`);
|
||||
}
|
||||
|
||||
if (parsed.labelNames.length > 0) {
|
||||
parts.push(`🏷️ ${parsed.labelNames.join(', ')}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue