From e3e3bbc24f6f77dbcbe187c8970865c3bb3b0599 Mon Sep 17 00:00:00 2001 From: vegard Date: Sun, 15 Mar 2026 22:15:36 +0100 Subject: [PATCH] MessageBox: universell meldingsboks-komponent for chat, kanban og kalender Felles MessageData-type (matcher messages-tabellen) med 3 renderingsmodi: - expanded (chat): forfatter, tid, HTML-body, mention-klikk, badges - compact (kanban): tittel, trunkert preview, fargestripe - calendar (pill): tidspunkt, tittel, bakgrunnsfarge Alle blokker (ChatBlock, KanbanBlock, CalendarBlock) migrert til MessageBox. PG-adaptere mapper API-respons til MessageData med view-spesifikke felter. SpacetimeDB-adapter oppdatert for kompatibilitet. Co-Authored-By: Claude Opus 4.6 --- web/src/lib/blocks/CalendarBlock.svelte | 82 +++----- web/src/lib/blocks/ChatBlock.svelte | 104 +-------- web/src/lib/blocks/KanbanBlock.svelte | 80 ++++--- web/src/lib/calendar/pg.svelte.ts | 58 ++++- web/src/lib/calendar/types.ts | 23 +- web/src/lib/chat/pg.svelte.ts | 27 ++- web/src/lib/chat/spacetime.svelte.ts | 46 +++- web/src/lib/chat/types.ts | 19 +- web/src/lib/components/MessageBox.svelte | 256 +++++++++++++++++++++++ web/src/lib/kanban/pg.svelte.ts | 73 ++++++- web/src/lib/kanban/types.ts | 18 +- web/src/lib/types/message.ts | 36 ++++ 12 files changed, 567 insertions(+), 255 deletions(-) create mode 100644 web/src/lib/components/MessageBox.svelte create mode 100644 web/src/lib/types/message.ts diff --git a/web/src/lib/blocks/CalendarBlock.svelte b/web/src/lib/blocks/CalendarBlock.svelte index 73d5b91..a5af616 100644 --- a/web/src/lib/blocks/CalendarBlock.svelte +++ b/web/src/lib/blocks/CalendarBlock.svelte @@ -1,7 +1,9 @@ + +{#if mode === 'expanded'} + + +
+ {#if showAuthor || showTimestamp} +
+ {#if showAuthor} + {message.author_name ?? 'Ukjent'} + {/if} + {#if showTimestamp} + {formatTime(message.created_at)} + {/if} + {#if message.pinned} + 📌 + {/if} +
+ {/if} +
{@html message.body}
+ {#if hasBadges} +
+ {#if message.kanban_view} + Kort + {/if} + {#if message.calendar_view} + Hendelse + {/if} +
+ {/if} +
+{:else if mode === 'compact'} + + +
+
{message.title ?? bodyPreview}
+ {#if message.body && message.title} +
{bodyPreview}
+ {/if} +
+{:else if mode === 'calendar'} + + +
+ {#if message.calendar_view && !message.calendar_view.all_day} + {formatTime(message.calendar_view.starts_at)} + {/if} + {message.title ?? stripHtml(message.body)} +
+{/if} + + diff --git a/web/src/lib/kanban/pg.svelte.ts b/web/src/lib/kanban/pg.svelte.ts index 6007ba3..af2ec30 100644 --- a/web/src/lib/kanban/pg.svelte.ts +++ b/web/src/lib/kanban/pg.svelte.ts @@ -1,4 +1,66 @@ -import type { KanbanBoard, KanbanConnection } from './types'; +import type { MessageData } from '$lib/types/message'; +import type { KanbanBoard, KanbanColumn, KanbanConnection } from './types'; + +interface RawCard { + id: string; + column_id: string; + title: string; + description: string | null; + assignee_id: string | null; + position: number; + created_by: string | null; + created_at: string; +} + +interface RawColumn { + id: string; + name: string; + color: string | null; + position: number; + cards: RawCard[]; +} + +interface RawBoard { + id: string; + name: string; + columns: RawColumn[]; +} + +function cardToMessageData(card: RawCard, column: RawColumn): MessageData { + return { + id: card.id, + channel_id: null, + reply_to: null, + author_id: card.created_by, + author_name: null, + message_type: 'kanban', + title: card.title, + body: card.description ?? '', + pinned: false, + visibility: 'workspace', + created_at: card.created_at, + updated_at: card.created_at, + kanban_view: { + column_id: card.column_id, + color: column.color + }, + calendar_view: null + }; +} + +function mapBoard(raw: RawBoard): KanbanBoard { + return { + id: raw.id, + name: raw.name, + columns: raw.columns.map((col): KanbanColumn => ({ + id: col.id, + name: col.name, + color: col.color, + position: col.position, + cards: col.cards.map(card => cardToMessageData(card, col)) + })) + }; +} /** * Kanban PG-adapter. @@ -16,7 +78,8 @@ export function createPgKanban(boardId: string): KanbanConnection { try { const res = await fetch(`/api/kanban/${boardId}`); if (!res.ok) throw new Error('Feil ved lasting'); - board = await res.json(); + const raw: RawBoard = await res.json(); + board = mapBoard(raw); error = ''; } catch { error = 'Kunne ikke laste kanban-brett'; @@ -64,9 +127,11 @@ export function createPgKanban(boardId: string): KanbanConnection { if (card) { const targetCol = updatedColumns.find(c => c.id === toColumnId); if (targetCol) { - const movedCard = { ...card, column_id: toColumnId, position }; + const movedCard = { + ...card, + kanban_view: { column_id: toColumnId, color: targetCol.color } + }; targetCol.cards.push(movedCard); - targetCol.cards.sort((a, b) => a.position - b.position); } } board = { ...board, columns: updatedColumns }; diff --git a/web/src/lib/kanban/types.ts b/web/src/lib/kanban/types.ts index e8b615d..fbb8698 100644 --- a/web/src/lib/kanban/types.ts +++ b/web/src/lib/kanban/types.ts @@ -1,20 +1,16 @@ -export interface KanbanCard { - id: string; - column_id: string; - title: string; - description: string | null; - assignee_id: string | null; - position: number; - created_by: string | null; - created_at: string; -} +import type { MessageData } from '$lib/types/message'; + +export type { MessageData }; + +/** Bakoverkompatibelt alias */ +export type KanbanCard = MessageData; export interface KanbanColumn { id: string; name: string; color: string | null; position: number; - cards: KanbanCard[]; + cards: MessageData[]; } export interface KanbanBoard { diff --git a/web/src/lib/types/message.ts b/web/src/lib/types/message.ts new file mode 100644 index 0000000..9f8c221 --- /dev/null +++ b/web/src/lib/types/message.ts @@ -0,0 +1,36 @@ +export interface MessageData { + id: string; + channel_id: string | null; + reply_to: string | null; + author_id: string | null; + author_name: string | null; + message_type: string; + title: string | null; + body: string; + pinned: boolean; + visibility: 'workspace' | 'private'; + created_at: string; + updated_at: string; + reply_count?: number; + reactions?: ReactionSummary[]; + kanban_view?: { column_id: string; board_name?: string; color: string | null } | null; + calendar_view?: { + starts_at: string; + ends_at: string | null; + all_day: boolean; + color: string | null; + } | null; +} + +export interface ReactionSummary { + reaction: string; + count: number; + user_reacted: boolean; +} + +export type MessageBoxMode = 'compact' | 'calendar' | 'expanded'; + +export interface MessageBoxCallbacks { + onClick?: (id: string) => void; + onMentionClick?: (entityId: string) => void; +}