diff --git a/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte b/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte index 9e34f379a..5f05cffca 100644 --- a/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte @@ -25,7 +25,8 @@ TagSelector, } from './form'; import { ContactSelector, focusTrap } from '@manacore/shared-ui'; - import { ManaLinkList } from '@manacore/shared-links/ui'; + import { ManaLinkList, ManaLinkPicker } from '@manacore/shared-links/ui'; + import { searchCrossApp } from '$lib/data/cross-app-search'; interface Props { task: Task; @@ -53,6 +54,8 @@ let storyPoints = $state(null); let effectiveDuration = $state(null); let funRating = $state(null); + // Link picker state + let showLinkPicker = $state(false); // Contact associations let assignee = $state([]); let involvedContacts = $state([]); @@ -318,10 +321,27 @@
- +
+ + +
+ (showLinkPicker = false)} + onSearch={searchCrossApp} + /> +
diff --git a/apps/todo/apps/web/src/lib/data/cross-app-search.ts b/apps/todo/apps/web/src/lib/data/cross-app-search.ts new file mode 100644 index 000000000..7ebd1d2b0 --- /dev/null +++ b/apps/todo/apps/web/src/lib/data/cross-app-search.ts @@ -0,0 +1,134 @@ +/** + * Cross-App Search for ManaLinkPicker + * + * Searches across multiple apps' IndexedDB databases to find + * records that can be linked to. Used by the ManaLinkPicker component. + */ + +import { createLocalStore, type BaseRecord } from '@manacore/local-store'; + +// ─── Minimal types for search ─────────────────────────────── + +interface SearchableRecord extends BaseRecord { + title?: string; + name?: string; + firstName?: string; + lastName?: string; + email?: string; + description?: string; + startDate?: string; + allDay?: boolean; + color?: string; + deletedAt?: string | null; +} + +interface SearchResult { + app: string; + collection: string; + id: string; + title: string; + subtitle?: string; + color?: string; +} + +// ─── Readers (opened lazily on first search) ──────────────── + +let calendarReader: ReturnType | null = null; +let contactsReader: ReturnType | null = null; + +async function getCalendarReader() { + if (!calendarReader) { + calendarReader = createLocalStore({ + appId: 'calendar', + collections: [{ name: 'events', indexes: ['startDate'] }], + }); + await calendarReader.initialize(); + } + return calendarReader; +} + +async function getContactsReader() { + if (!contactsReader) { + contactsReader = createLocalStore({ + appId: 'contacts', + collections: [{ name: 'contacts', indexes: ['firstName', 'lastName', 'email'] }], + }); + await contactsReader.initialize(); + } + return contactsReader; +} + +// ─── Search ───────────────────────────────────────────────── + +function matchesQuery(record: SearchableRecord, q: string): boolean { + const lower = q.toLowerCase(); + const fields = [ + record.title, + record.name, + record.firstName, + record.lastName, + record.email, + record.description, + ]; + return fields.some((f) => f?.toLowerCase().includes(lower)); +} + +/** + * Search across calendar events and contacts. + * Todo tasks are excluded since we're linking FROM a todo task. + */ +export async function searchCrossApp(query: string): Promise { + const results: SearchResult[] = []; + const q = query.toLowerCase(); + + // Search calendar events + try { + const cal = await getCalendarReader(); + const events = await cal.collection('events').getAll(); + for (const event of events) { + if (event.deletedAt) continue; + if (matchesQuery(event, q)) { + const startDate = event.startDate + ? new Date(event.startDate).toLocaleDateString('de-DE', { + day: 'numeric', + month: 'short', + }) + : undefined; + results.push({ + app: 'calendar', + collection: 'events', + id: event.id, + title: event.title ?? 'Termin', + subtitle: startDate, + color: (event.color as string) ?? '#3B82F6', + }); + } + } + } catch { + // Calendar DB may not exist yet + } + + // Search contacts + try { + const con = await getContactsReader(); + const contacts = await con.collection('contacts').getAll(); + for (const contact of contacts) { + if (contact.deletedAt) continue; + if (matchesQuery(contact, q)) { + const name = [contact.firstName, contact.lastName].filter(Boolean).join(' '); + results.push({ + app: 'contacts', + collection: 'contacts', + id: contact.id, + title: name || contact.email || 'Kontakt', + subtitle: contact.email, + color: '#EC4899', + }); + } + } + } catch { + // Contacts DB may not exist yet + } + + return results.slice(0, 20); +} diff --git a/packages/shared-links/src/ui/ManaLinkPicker.svelte b/packages/shared-links/src/ui/ManaLinkPicker.svelte new file mode 100644 index 000000000..cee10cb11 --- /dev/null +++ b/packages/shared-links/src/ui/ManaLinkPicker.svelte @@ -0,0 +1,322 @@ + + +{#if open} + + +{/if} + + diff --git a/packages/shared-links/src/ui/index.ts b/packages/shared-links/src/ui/index.ts index c0e359298..44bcc8c57 100644 --- a/packages/shared-links/src/ui/index.ts +++ b/packages/shared-links/src/ui/index.ts @@ -1,2 +1,3 @@ export { default as ManaLinkBadge } from './ManaLinkBadge.svelte'; export { default as ManaLinkList } from './ManaLinkList.svelte'; +export { default as ManaLinkPicker } from './ManaLinkPicker.svelte';