@open-mercato/ui 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4397.1.9a65481757
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/backend/CrudForm.js +27 -7
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/DataTable.js +11 -4
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +4 -0
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/messages/EmailThreadsPanel.js +212 -0
- package/dist/backend/messages/EmailThreadsPanel.js.map +7 -0
- package/dist/backend/messages/index.js +4 -0
- package/dist/backend/messages/index.js.map +2 -2
- package/dist/backend/messages/mergeOptimisticEmailThreads.js +55 -0
- package/dist/backend/messages/mergeOptimisticEmailThreads.js.map +7 -0
- package/dist/backend/notifications/NotificationDispatcher.js +1 -0
- package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
- package/dist/backend/notifications/useNotificationsPoll.js +7 -0
- package/dist/backend/notifications/useNotificationsPoll.js.map +2 -2
- package/dist/backend/notifications/useNotificationsSse.js +8 -0
- package/dist/backend/notifications/useNotificationsSse.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/CrudForm.tsx +33 -7
- package/src/backend/DataTable.tsx +34 -4
- package/src/backend/__tests__/CrudForm.unsavedNavigation.test.tsx +51 -2
- package/src/backend/icons/lucideRegistry.generated.tsx +4 -0
- package/src/backend/messages/EmailThreadsPanel.tsx +351 -0
- package/src/backend/messages/__tests__/mergeOptimisticEmailThreads.test.ts +126 -0
- package/src/backend/messages/index.ts +12 -0
- package/src/backend/messages/mergeOptimisticEmailThreads.ts +84 -0
- package/src/backend/notifications/NotificationDispatcher.ts +2 -0
- package/src/backend/notifications/useNotificationsPoll.ts +5 -0
- package/src/backend/notifications/useNotificationsSse.ts +6 -0
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
Mail,
|
|
79
79
|
MailOpen,
|
|
80
80
|
MapPin,
|
|
81
|
+
MessageCircle,
|
|
81
82
|
MinusCircle,
|
|
82
83
|
Notebook,
|
|
83
84
|
Package,
|
|
@@ -93,6 +94,7 @@ import {
|
|
|
93
94
|
Receipt,
|
|
94
95
|
ReceiptText,
|
|
95
96
|
RefreshCcw,
|
|
97
|
+
RefreshCw,
|
|
96
98
|
Reply,
|
|
97
99
|
RotateCcw,
|
|
98
100
|
Ruler,
|
|
@@ -217,6 +219,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
217
219
|
"mail": Mail,
|
|
218
220
|
"mail-open": MailOpen,
|
|
219
221
|
"map-pin": MapPin,
|
|
222
|
+
"message-circle": MessageCircle,
|
|
220
223
|
"minus-circle": MinusCircle,
|
|
221
224
|
"notebook": Notebook,
|
|
222
225
|
"package": Package,
|
|
@@ -232,6 +235,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
232
235
|
"receipt": Receipt,
|
|
233
236
|
"receipt-text": ReceiptText,
|
|
234
237
|
"refresh-ccw": RefreshCcw,
|
|
238
|
+
"refresh-cw": RefreshCw,
|
|
235
239
|
"reply": Reply,
|
|
236
240
|
"rotate-ccw": RotateCcw,
|
|
237
241
|
"ruler": Ruler,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/icons/lucideRegistry.generated.tsx"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Activity,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Archive,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Bookmark,\n Box,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CreditCard,\n Crown,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n Eye,\n FileMinus,\n FilePenLine,\n FileSpreadsheet,\n FileText,\n FilterX,\n Flag,\n Flame,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hand,\n Handshake,\n Headphones,\n Heart,\n HelpCircle,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Lightbulb,\n LineChart,\n Link,\n List,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n MapPin,\n MinusCircle,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageX,\n PauseCircle,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Share2,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Snowflake,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Timer,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n UserX,\n Wallet,\n Webhook,\n Wrench,\n X,\n XCircle,\n} from 'lucide-react'\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'archive': Archive,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'bookmark': Bookmark,\n 'box': Box,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'credit-card': CreditCard,\n 'crown': Crown,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'eye': Eye,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-spreadsheet': FileSpreadsheet,\n 'file-text': FileText,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'flame': Flame,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hand': Hand,\n 'handshake': Handshake,\n 'headphones': Headphones,\n 'heart': Heart,\n 'help-circle': HelpCircle,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map-pin': MapPin,\n 'minus-circle': MinusCircle,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'share-2': Share2,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'snowflake': Snowflake,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'timer': Timer,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'user-x': UserX,\n 'users': Users,\n 'wallet': Wallet,\n 'webhook': Webhook,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n}\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed\n if (!withoutPrefix) return ''\n if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {\n return withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n }\n return withoutPrefix\n .replace(/[_\\s]+/g, '-')\n .replace(/-+/g, '-')\n .toLowerCase()\n}\n\nexport function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {\n if (!name) return null\n const normalized = normalizeKebabIconName(name)\n if (!normalized) return null\n return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null\n}\n\nexport function resolveRegisteredLucideIconNode(\n name: string | undefined,\n className: string\n): React.ReactNode | null {\n const Icon = resolveRegisteredLucideIcon(name)\n if (!Icon) return null\n return <Icon className={className} />\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Activity,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Archive,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Bookmark,\n Box,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CreditCard,\n Crown,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n Eye,\n FileMinus,\n FilePenLine,\n FileSpreadsheet,\n FileText,\n FilterX,\n Flag,\n Flame,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hand,\n Handshake,\n Headphones,\n Heart,\n HelpCircle,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Lightbulb,\n LineChart,\n Link,\n List,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n MapPin,\n MessageCircle,\n MinusCircle,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageX,\n PauseCircle,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n RefreshCw,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Share2,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Snowflake,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Timer,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n UserX,\n Wallet,\n Webhook,\n Wrench,\n X,\n XCircle,\n} from 'lucide-react'\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'archive': Archive,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'bookmark': Bookmark,\n 'box': Box,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'credit-card': CreditCard,\n 'crown': Crown,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'eye': Eye,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-spreadsheet': FileSpreadsheet,\n 'file-text': FileText,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'flame': Flame,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hand': Hand,\n 'handshake': Handshake,\n 'headphones': Headphones,\n 'heart': Heart,\n 'help-circle': HelpCircle,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map-pin': MapPin,\n 'message-circle': MessageCircle,\n 'minus-circle': MinusCircle,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'refresh-cw': RefreshCw,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'share-2': Share2,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'snowflake': Snowflake,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'timer': Timer,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'user-x': UserX,\n 'users': Users,\n 'wallet': Wallet,\n 'webhook': Webhook,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n}\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed\n if (!withoutPrefix) return ''\n if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {\n return withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n }\n return withoutPrefix\n .replace(/[_\\s]+/g, '-')\n .replace(/-+/g, '-')\n .toLowerCase()\n}\n\nexport function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {\n if (!name) return null\n const normalized = normalizeKebabIconName(name)\n if (!normalized) return null\n return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null\n}\n\nexport function resolveRegisteredLucideIconNode(\n name: string | undefined,\n className: string\n): React.ReactNode | null {\n const Icon = resolveRegisteredLucideIcon(name)\n if (!Icon) return null\n return <Icon className={className} />\n}\n"],
|
|
5
|
+
"mappings": "AAgUS;AA1TT;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAC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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ArrowDownLeft, ArrowUpRight, Check, Mail, RefreshCw, Reply, TriangleAlert } from "lucide-react";
|
|
5
|
+
import { Button } from "../../primitives/button.js";
|
|
6
|
+
import { Badge } from "../../primitives/badge.js";
|
|
7
|
+
import { Spinner } from "../../primitives/spinner.js";
|
|
8
|
+
import { Alert, AlertDescription } from "../../primitives/alert.js";
|
|
9
|
+
import { EmptyState } from "../EmptyState.js";
|
|
10
|
+
import { formatDateTime } from "@open-mercato/shared/lib/time";
|
|
11
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
12
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
13
|
+
function formatWhen(value) {
|
|
14
|
+
return formatDateTime(value) ?? value;
|
|
15
|
+
}
|
|
16
|
+
function EmailThreadsPanel({
|
|
17
|
+
threads,
|
|
18
|
+
loading = false,
|
|
19
|
+
error = null,
|
|
20
|
+
canCompose = false,
|
|
21
|
+
composeDisabledHint,
|
|
22
|
+
onComposeNew,
|
|
23
|
+
onReply,
|
|
24
|
+
onRefresh,
|
|
25
|
+
onRetry,
|
|
26
|
+
className
|
|
27
|
+
}) {
|
|
28
|
+
const t = useT();
|
|
29
|
+
const [selectedKey, setSelectedKey] = React.useState(null);
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (threads.length === 0) {
|
|
32
|
+
setSelectedKey(null);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setSelectedKey(
|
|
36
|
+
(current) => current && threads.some((thread) => thread.threadKey === current) ? current : threads[0].threadKey
|
|
37
|
+
);
|
|
38
|
+
}, [threads]);
|
|
39
|
+
const selected = React.useMemo(
|
|
40
|
+
() => threads.find((thread) => thread.threadKey === selectedKey) ?? null,
|
|
41
|
+
[threads, selectedKey]
|
|
42
|
+
);
|
|
43
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-4", className), children: [
|
|
44
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
45
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("ui.email.threads.count", "{count} conversations", { count: threads.length }) }),
|
|
46
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
47
|
+
onRefresh ? /* @__PURE__ */ jsxs(
|
|
48
|
+
Button,
|
|
49
|
+
{
|
|
50
|
+
type: "button",
|
|
51
|
+
variant: "outline",
|
|
52
|
+
size: "sm",
|
|
53
|
+
className: "gap-2",
|
|
54
|
+
onClick: onRefresh,
|
|
55
|
+
disabled: loading,
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: cn("h-4 w-4", loading && "animate-spin") }),
|
|
58
|
+
t("ui.email.threads.refresh", "Refresh")
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
) : null,
|
|
62
|
+
canCompose && onComposeNew ? /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", className: "gap-2", onClick: onComposeNew, children: [
|
|
63
|
+
/* @__PURE__ */ jsx(Mail, { className: "h-4 w-4" }),
|
|
64
|
+
t("ui.email.threads.new", "New email")
|
|
65
|
+
] }) : null
|
|
66
|
+
] })
|
|
67
|
+
] }),
|
|
68
|
+
!canCompose && composeDisabledHint ? /* @__PURE__ */ jsx("div", { children: composeDisabledHint }) : null,
|
|
69
|
+
error ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null,
|
|
70
|
+
loading && threads.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground", children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
|
|
72
|
+
t("ui.email.threads.loading", "Loading conversations\u2026")
|
|
73
|
+
] }) : threads.length === 0 ? /* @__PURE__ */ jsx(
|
|
74
|
+
EmptyState,
|
|
75
|
+
{
|
|
76
|
+
icon: /* @__PURE__ */ jsx(Mail, { className: "h-6 w-6" }),
|
|
77
|
+
title: t("ui.email.threads.empty.title", "No emails yet"),
|
|
78
|
+
description: t(
|
|
79
|
+
"ui.email.threads.empty.description",
|
|
80
|
+
"Emails you send or receive with this contact will appear here, grouped into conversations."
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
) : /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [
|
|
84
|
+
/* @__PURE__ */ jsx("div", { className: "md:col-span-1", children: /* @__PURE__ */ jsx("ul", { className: "max-h-96 divide-y divide-border overflow-y-auto rounded-md border border-border", children: threads.map((thread) => {
|
|
85
|
+
const isSelected = thread.threadKey === selectedKey;
|
|
86
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
87
|
+
Button,
|
|
88
|
+
{
|
|
89
|
+
type: "button",
|
|
90
|
+
variant: "ghost",
|
|
91
|
+
onClick: () => setSelectedKey(thread.threadKey),
|
|
92
|
+
className: cn(
|
|
93
|
+
"h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-none p-3 text-left hover:bg-muted",
|
|
94
|
+
isSelected ? "bg-muted" : "bg-transparent"
|
|
95
|
+
),
|
|
96
|
+
"aria-current": isSelected,
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-foreground", children: thread.subject || t("ui.email.threads.noSubject", "(no subject)") }),
|
|
100
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatWhen(thread.lastMessageAt) })
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
103
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: thread.participants.join(", ") || t("ui.email.threads.unknownParticipant", "Unknown sender") }),
|
|
104
|
+
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "shrink-0", children: thread.messageCount })
|
|
105
|
+
] }),
|
|
106
|
+
thread.preview ? /* @__PURE__ */ jsx("span", { className: "line-clamp-1 text-xs text-muted-foreground", children: thread.preview }) : null
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
) }, thread.threadKey);
|
|
110
|
+
}) }) }),
|
|
111
|
+
/* @__PURE__ */ jsx("div", { className: "md:col-span-2", children: selected ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded-md border border-border p-4", children: [
|
|
112
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
113
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-foreground", children: selected.subject || t("ui.email.threads.noSubject", "(no subject)") }),
|
|
114
|
+
canCompose && onReply ? /* @__PURE__ */ jsxs(
|
|
115
|
+
Button,
|
|
116
|
+
{
|
|
117
|
+
type: "button",
|
|
118
|
+
variant: "outline",
|
|
119
|
+
size: "sm",
|
|
120
|
+
className: "shrink-0 gap-2",
|
|
121
|
+
onClick: () => onReply(selected),
|
|
122
|
+
children: [
|
|
123
|
+
/* @__PURE__ */ jsx(Reply, { className: "h-4 w-4" }),
|
|
124
|
+
t("ui.email.threads.reply", "Reply")
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
) : null
|
|
128
|
+
] }),
|
|
129
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: selected.messages.map((message) => /* @__PURE__ */ jsx(EmailMessageCard, { message, onRetry }, message.id)) })
|
|
130
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-md border border-border p-10 text-sm text-muted-foreground", children: t("ui.email.threads.selectPrompt", "Select a conversation to read it.") }) })
|
|
131
|
+
] })
|
|
132
|
+
] });
|
|
133
|
+
}
|
|
134
|
+
function EmailMessageCard({
|
|
135
|
+
message,
|
|
136
|
+
onRetry
|
|
137
|
+
}) {
|
|
138
|
+
const t = useT();
|
|
139
|
+
const isOutbound = message.direction === "outbound";
|
|
140
|
+
const fromLabel = message.fromName ? `${message.fromName}${message.fromEmail ? ` <${message.fromEmail}>` : ""}` : message.fromEmail ?? t("ui.email.threads.unknownParticipant", "Unknown sender");
|
|
141
|
+
return /* @__PURE__ */ jsxs(
|
|
142
|
+
"div",
|
|
143
|
+
{
|
|
144
|
+
className: `rounded-md border border-border p-3 ${isOutbound ? "bg-accent" : "bg-muted"}`,
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-start justify-between gap-2", children: [
|
|
147
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
148
|
+
isOutbound ? /* @__PURE__ */ jsx(ArrowUpRight, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(ArrowDownLeft, { className: "h-3.5 w-3.5" }),
|
|
149
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: isOutbound ? t("ui.email.threads.you", "You") : fromLabel }),
|
|
150
|
+
message.to.length > 0 ? /* @__PURE__ */ jsx("span", { className: "truncate", children: t("ui.email.threads.toLabel", "to {recipients}", {
|
|
151
|
+
recipients: message.to.join(", ")
|
|
152
|
+
}) }) : null
|
|
153
|
+
] }),
|
|
154
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatWhen(message.sentAt) })
|
|
155
|
+
] }),
|
|
156
|
+
/* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: message.bodyText || t("ui.email.threads.noBody", "(no content)") }),
|
|
157
|
+
/* @__PURE__ */ jsx(EmailMessageStatus, { message, onRetry })
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
function EmailMessageStatus({
|
|
163
|
+
message,
|
|
164
|
+
onRetry
|
|
165
|
+
}) {
|
|
166
|
+
const t = useT();
|
|
167
|
+
if (message.status === "sending") {
|
|
168
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-1.5 text-xs text-muted-foreground", "aria-live": "polite", children: [
|
|
169
|
+
/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }),
|
|
170
|
+
t("ui.email.threads.status.sending", "Sending\u2026")
|
|
171
|
+
] });
|
|
172
|
+
}
|
|
173
|
+
if (message.status === "sent") {
|
|
174
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-1.5 text-xs text-status-success-text", "aria-live": "polite", children: [
|
|
175
|
+
/* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-status-success-icon" }),
|
|
176
|
+
t("ui.email.threads.status.sent", "Sent")
|
|
177
|
+
] });
|
|
178
|
+
}
|
|
179
|
+
if (message.status === "failed") {
|
|
180
|
+
return /* @__PURE__ */ jsxs(
|
|
181
|
+
"div",
|
|
182
|
+
{
|
|
183
|
+
className: "mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text",
|
|
184
|
+
"aria-live": "polite",
|
|
185
|
+
children: [
|
|
186
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
187
|
+
/* @__PURE__ */ jsx(TriangleAlert, { className: "h-3.5 w-3.5 text-status-error-icon" }),
|
|
188
|
+
message.statusError || t("ui.email.threads.status.failed", "Not delivered")
|
|
189
|
+
] }),
|
|
190
|
+
onRetry ? /* @__PURE__ */ jsx(
|
|
191
|
+
Button,
|
|
192
|
+
{
|
|
193
|
+
type: "button",
|
|
194
|
+
variant: "outline",
|
|
195
|
+
size: "sm",
|
|
196
|
+
className: "h-auto px-2 py-0.5 text-xs",
|
|
197
|
+
onClick: () => onRetry(message),
|
|
198
|
+
children: t("ui.email.threads.status.retry", "Retry")
|
|
199
|
+
}
|
|
200
|
+
) : null
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
var EmailThreadsPanel_default = EmailThreadsPanel;
|
|
208
|
+
export {
|
|
209
|
+
EmailThreadsPanel,
|
|
210
|
+
EmailThreadsPanel_default as default
|
|
211
|
+
};
|
|
212
|
+
//# sourceMappingURL=EmailThreadsPanel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/messages/EmailThreadsPanel.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ArrowDownLeft, ArrowUpRight, Check, Mail, RefreshCw, Reply, TriangleAlert } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { Badge } from '../../primitives/badge'\nimport { Spinner } from '../../primitives/spinner'\nimport { Alert, AlertDescription } from '../../primitives/alert'\nimport { EmptyState } from '../EmptyState'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type EmailThreadDirection = 'inbound' | 'outbound'\n\n/**\n * Optimistic delivery status for a just-sent outbound message. Server-confirmed\n * messages leave this `undefined` and render without a status indicator.\n */\nexport type EmailThreadMessageStatus = 'sending' | 'sent' | 'failed'\n\nexport type EmailThreadMessage = {\n id: string\n messageId: string | null\n rfcMessageId: string | null\n references: string[]\n direction: EmailThreadDirection\n fromName: string | null\n fromEmail: string | null\n to: string[]\n cc: string[]\n subject: string | null\n bodyText: string | null\n sentAt: string\n providerKey: string | null\n /** Optimistic send status; absent on server-confirmed messages. */\n status?: EmailThreadMessageStatus\n /** Human-readable failure reason shown when `status === 'failed'`. */\n statusError?: string | null\n}\n\nexport type EmailThread = {\n threadKey: string\n subject: string | null\n preview: string | null\n participants: string[]\n lastMessageAt: string\n messageCount: number\n providerKey: string | null\n lastDirection: EmailThreadDirection\n messages: EmailThreadMessage[]\n}\n\nexport type EmailThreadsPanelProps = {\n threads: EmailThread[]\n loading?: boolean\n error?: string | null\n /** When false, compose/reply controls are hidden and `composeDisabledHint` is shown. */\n canCompose?: boolean\n composeDisabledHint?: React.ReactNode\n onComposeNew?: () => void\n onReply?: (thread: EmailThread) => void\n onRefresh?: () => void\n /** Invoked when the user retries a message whose `status === 'failed'`. */\n onRetry?: (message: EmailThreadMessage) => void\n className?: string\n}\n\nfunction formatWhen(value: string): string {\n return formatDateTime(value) ?? value\n}\n\n/**\n * Entity-agnostic Gmail-style email thread viewer. The host supplies the\n * already-fetched threads plus compose/reply/refresh handlers; this component\n * owns only the master/detail layout, selection, and conversation rendering, so\n * it can be dropped onto the CRM Person page, a Company page, or any module.\n */\nexport function EmailThreadsPanel({\n threads,\n loading = false,\n error = null,\n canCompose = false,\n composeDisabledHint,\n onComposeNew,\n onReply,\n onRefresh,\n onRetry,\n className,\n}: EmailThreadsPanelProps) {\n const t = useT()\n const [selectedKey, setSelectedKey] = React.useState<string | null>(null)\n\n // Keep a valid selection as the thread set changes (refresh, new email).\n React.useEffect(() => {\n if (threads.length === 0) {\n setSelectedKey(null)\n return\n }\n setSelectedKey((current) =>\n current && threads.some((thread) => thread.threadKey === current)\n ? current\n : threads[0].threadKey,\n )\n }, [threads])\n\n const selected = React.useMemo(\n () => threads.find((thread) => thread.threadKey === selectedKey) ?? null,\n [threads, selectedKey],\n )\n\n return (\n <div className={cn('flex flex-col gap-4', className)}>\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t('ui.email.threads.count', '{count} conversations', { count: threads.length })}\n </div>\n <div className=\"flex items-center gap-2\">\n {onRefresh ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n onClick={onRefresh}\n disabled={loading}\n >\n <RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />\n {t('ui.email.threads.refresh', 'Refresh')}\n </Button>\n ) : null}\n {canCompose && onComposeNew ? (\n <Button type=\"button\" size=\"sm\" className=\"gap-2\" onClick={onComposeNew}>\n <Mail className=\"h-4 w-4\" />\n {t('ui.email.threads.new', 'New email')}\n </Button>\n ) : null}\n </div>\n </div>\n\n {!canCompose && composeDisabledHint ? <div>{composeDisabledHint}</div> : null}\n\n {error ? (\n <Alert variant=\"destructive\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n {loading && threads.length === 0 ? (\n <div className=\"flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('ui.email.threads.loading', 'Loading conversations\u2026')}\n </div>\n ) : threads.length === 0 ? (\n <EmptyState\n icon={<Mail className=\"h-6 w-6\" />}\n title={t('ui.email.threads.empty.title', 'No emails yet')}\n description={t(\n 'ui.email.threads.empty.description',\n 'Emails you send or receive with this contact will appear here, grouped into conversations.',\n )}\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-3\">\n {/* Master: thread list */}\n <div className=\"md:col-span-1\">\n <ul className=\"max-h-96 divide-y divide-border overflow-y-auto rounded-md border border-border\">\n {threads.map((thread) => {\n const isSelected = thread.threadKey === selectedKey\n return (\n <li key={thread.threadKey}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => setSelectedKey(thread.threadKey)}\n className={cn(\n 'h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-none p-3 text-left hover:bg-muted',\n isSelected ? 'bg-muted' : 'bg-transparent',\n )}\n aria-current={isSelected}\n >\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"truncate text-sm font-medium text-foreground\">\n {thread.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatWhen(thread.lastMessageAt)}\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-muted-foreground\">\n {thread.participants.join(', ') ||\n t('ui.email.threads.unknownParticipant', 'Unknown sender')}\n </span>\n <Badge variant=\"secondary\" className=\"shrink-0\">\n {thread.messageCount}\n </Badge>\n </div>\n {thread.preview ? (\n <span className=\"line-clamp-1 text-xs text-muted-foreground\">\n {thread.preview}\n </span>\n ) : null}\n </Button>\n </li>\n )\n })}\n </ul>\n </div>\n\n {/* Detail: conversation */}\n <div className=\"md:col-span-2\">\n {selected ? (\n <div className=\"flex flex-col gap-3 rounded-md border border-border p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"text-base font-semibold text-foreground\">\n {selected.subject || t('ui.email.threads.noSubject', '(no subject)')}\n </h3>\n {canCompose && onReply ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"shrink-0 gap-2\"\n onClick={() => onReply(selected)}\n >\n <Reply className=\"h-4 w-4\" />\n {t('ui.email.threads.reply', 'Reply')}\n </Button>\n ) : null}\n </div>\n <div className=\"flex flex-col gap-3\">\n {selected.messages.map((message) => (\n <EmailMessageCard key={message.id} message={message} onRetry={onRetry} />\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-center rounded-md border border-border p-10 text-sm text-muted-foreground\">\n {t('ui.email.threads.selectPrompt', 'Select a conversation to read it.')}\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nfunction EmailMessageCard({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n const isOutbound = message.direction === 'outbound'\n const fromLabel = message.fromName\n ? `${message.fromName}${message.fromEmail ? ` <${message.fromEmail}>` : ''}`\n : message.fromEmail ?? t('ui.email.threads.unknownParticipant', 'Unknown sender')\n return (\n <div\n className={`rounded-md border border-border p-3 ${isOutbound ? 'bg-accent' : 'bg-muted'}`}\n >\n <div className=\"mb-2 flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n {isOutbound ? (\n <ArrowUpRight className=\"h-3.5 w-3.5\" />\n ) : (\n <ArrowDownLeft className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"font-medium text-foreground\">\n {isOutbound ? t('ui.email.threads.you', 'You') : fromLabel}\n </span>\n {message.to.length > 0 ? (\n <span className=\"truncate\">\n {t('ui.email.threads.toLabel', 'to {recipients}', {\n recipients: message.to.join(', '),\n })}\n </span>\n ) : null}\n </div>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatWhen(message.sentAt)}</span>\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">\n {message.bodyText || t('ui.email.threads.noBody', '(no content)')}\n </div>\n <EmailMessageStatus message={message} onRetry={onRetry} />\n </div>\n )\n}\n\n/**\n * Renders the optimistic send-status footer for an outbound message: a spinner\n * while sending, a success check once confirmed, or an error with a Retry action\n * on delivery failure. Server-confirmed messages have no `status` and render\n * nothing here.\n */\nfunction EmailMessageStatus({\n message,\n onRetry,\n}: {\n message: EmailThreadMessage\n onRetry?: (message: EmailThreadMessage) => void\n}) {\n const t = useT()\n if (message.status === 'sending') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-muted-foreground\" aria-live=\"polite\">\n <Spinner className=\"h-3 w-3\" />\n {t('ui.email.threads.status.sending', 'Sending\u2026')}\n </div>\n )\n }\n if (message.status === 'sent') {\n return (\n <div className=\"mt-2 flex items-center gap-1.5 text-xs text-status-success-text\" aria-live=\"polite\">\n <Check className=\"h-3.5 w-3.5 text-status-success-icon\" />\n {t('ui.email.threads.status.sent', 'Sent')}\n </div>\n )\n }\n if (message.status === 'failed') {\n return (\n <div\n className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text\"\n aria-live=\"polite\"\n >\n <span className=\"flex items-center gap-1.5\">\n <TriangleAlert className=\"h-3.5 w-3.5 text-status-error-icon\" />\n {message.statusError || t('ui.email.threads.status.failed', 'Not delivered')}\n </span>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto px-2 py-0.5 text-xs\"\n onClick={() => onRetry(message)}\n >\n {t('ui.email.threads.status.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )\n }\n return null\n}\n\nexport default EmailThreadsPanel\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -7,13 +7,17 @@ import { useMessagesPoll } from "./useMessagesPoll.js";
|
|
|
7
7
|
import { useMessagesSse } from "./useMessagesSse.js";
|
|
8
8
|
import { MessageObjectPreview } from "./MessageObjectPreview.js";
|
|
9
9
|
import { MessageObjectDetail } from "./MessageObjectDetail.js";
|
|
10
|
+
import { EmailThreadsPanel } from "./EmailThreadsPanel.js";
|
|
11
|
+
import { mergeOptimisticEmailThreads } from "./mergeOptimisticEmailThreads.js";
|
|
10
12
|
export {
|
|
13
|
+
EmailThreadsPanel,
|
|
11
14
|
MessageComposer,
|
|
12
15
|
MessageObjectDetail,
|
|
13
16
|
MessageObjectPreview,
|
|
14
17
|
MessageObjectRecordPicker,
|
|
15
18
|
MessagesIcon,
|
|
16
19
|
SendObjectMessageDialog,
|
|
20
|
+
mergeOptimisticEmailThreads,
|
|
17
21
|
useMessages,
|
|
18
22
|
useMessagesPoll,
|
|
19
23
|
useMessagesSse
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/messages/index.ts"],
|
|
4
|
-
"sourcesContent": ["export { MessageComposer } from './MessageComposer'\nexport type {\n MessageComposerContextObject,\n MessageComposerProps,\n MessageComposerVariant,\n MessageTypeItem,\n} from './MessageComposer'\n\nexport { SendObjectMessageDialog } from './SendObjectMessageDialog'\nexport type { SendObjectMessageDialogProps } from './SendObjectMessageDialog'\n\nexport { MessageObjectRecordPicker } from './MessageObjectRecordPicker'\nexport type { MessageObjectRecordPickerProps } from './MessageObjectRecordPicker'\n\nexport { MessagesIcon } from './MessagesIcon'\nexport type { MessagesIconProps } from './MessagesIcon'\n\nexport { useMessages } from './useMessages'\nexport { useMessagesPoll } from './useMessagesPoll'\nexport { useMessagesSse } from './useMessagesSse'\nexport type { MessagePollItem, UseMessagesPollResult } from './useMessagesPoll'\n\nexport { MessageObjectPreview } from './MessageObjectPreview'\nexport { MessageObjectDetail } from './MessageObjectDetail'\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,uBAAuB;AAQhC,SAAS,+BAA+B;AAGxC,SAAS,iCAAiC;AAG1C,SAAS,oBAAoB;AAG7B,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAG/B,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;",
|
|
4
|
+
"sourcesContent": ["export { MessageComposer } from './MessageComposer'\nexport type {\n MessageComposerContextObject,\n MessageComposerProps,\n MessageComposerVariant,\n MessageTypeItem,\n} from './MessageComposer'\n\nexport { SendObjectMessageDialog } from './SendObjectMessageDialog'\nexport type { SendObjectMessageDialogProps } from './SendObjectMessageDialog'\n\nexport { MessageObjectRecordPicker } from './MessageObjectRecordPicker'\nexport type { MessageObjectRecordPickerProps } from './MessageObjectRecordPicker'\n\nexport { MessagesIcon } from './MessagesIcon'\nexport type { MessagesIconProps } from './MessagesIcon'\n\nexport { useMessages } from './useMessages'\nexport { useMessagesPoll } from './useMessagesPoll'\nexport { useMessagesSse } from './useMessagesSse'\nexport type { MessagePollItem, UseMessagesPollResult } from './useMessagesPoll'\n\nexport { MessageObjectPreview } from './MessageObjectPreview'\nexport { MessageObjectDetail } from './MessageObjectDetail'\n\nexport { EmailThreadsPanel } from './EmailThreadsPanel'\nexport type {\n EmailThread,\n EmailThreadMessage,\n EmailThreadMessageStatus,\n EmailThreadDirection,\n EmailThreadsPanelProps,\n} from './EmailThreadsPanel'\n\nexport { mergeOptimisticEmailThreads } from './mergeOptimisticEmailThreads'\nexport type { OptimisticEmailMessage } from './mergeOptimisticEmailThreads'\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB;AAQhC,SAAS,+BAA+B;AAGxC,SAAS,iCAAiC;AAG1C,SAAS,oBAAoB;AAG7B,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAG/B,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AAEpC,SAAS,yBAAyB;AASlC,SAAS,mCAAmC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function mergeOptimisticEmailThreads(serverThreads, optimistic) {
|
|
2
|
+
if (optimistic.length === 0) return serverThreads;
|
|
3
|
+
const serverMessageIds = /* @__PURE__ */ new Set();
|
|
4
|
+
for (const thread of serverThreads) {
|
|
5
|
+
for (const message of thread.messages) {
|
|
6
|
+
if (message.messageId) serverMessageIds.add(message.messageId);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const pending = optimistic.filter(
|
|
10
|
+
(entry) => !(entry.messageId && serverMessageIds.has(entry.messageId))
|
|
11
|
+
);
|
|
12
|
+
if (pending.length === 0) return serverThreads;
|
|
13
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
14
|
+
const order = [];
|
|
15
|
+
for (const thread of serverThreads) {
|
|
16
|
+
byKey.set(thread.threadKey, thread);
|
|
17
|
+
order.push(thread.threadKey);
|
|
18
|
+
}
|
|
19
|
+
for (const entry of pending) {
|
|
20
|
+
const { threadKey, ...message } = entry;
|
|
21
|
+
const existing = byKey.get(threadKey);
|
|
22
|
+
if (existing) {
|
|
23
|
+
if (existing.messages.some((m) => m.id === message.id)) continue;
|
|
24
|
+
const messages = [...existing.messages, message].sort(
|
|
25
|
+
(a, b) => a.sentAt.localeCompare(b.sentAt)
|
|
26
|
+
);
|
|
27
|
+
const last = messages[messages.length - 1];
|
|
28
|
+
byKey.set(threadKey, {
|
|
29
|
+
...existing,
|
|
30
|
+
messages,
|
|
31
|
+
messageCount: messages.length,
|
|
32
|
+
lastMessageAt: last?.sentAt ?? existing.lastMessageAt,
|
|
33
|
+
lastDirection: last?.direction ?? existing.lastDirection
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
byKey.set(threadKey, {
|
|
37
|
+
threadKey,
|
|
38
|
+
subject: message.subject,
|
|
39
|
+
preview: message.bodyText ? message.bodyText.slice(0, 140) : null,
|
|
40
|
+
participants: message.to,
|
|
41
|
+
lastMessageAt: message.sentAt,
|
|
42
|
+
messageCount: 1,
|
|
43
|
+
providerKey: message.providerKey,
|
|
44
|
+
lastDirection: message.direction,
|
|
45
|
+
messages: [message]
|
|
46
|
+
});
|
|
47
|
+
order.push(threadKey);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return order.map((key) => byKey.get(key)).sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
mergeOptimisticEmailThreads
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=mergeOptimisticEmailThreads.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/messages/mergeOptimisticEmailThreads.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EmailThread, EmailThreadMessage } from './EmailThreadsPanel'\n\n/**\n * An optimistic (client-side, not-yet-server-confirmed) outbound message plus\n * the key of the thread it belongs to. `message.messageId` is the Open Mercato\n * Message id returned by the send API \u2014 it is the reconciliation key against the\n * server-fetched threads.\n */\nexport type OptimisticEmailMessage = EmailThreadMessage & { threadKey: string }\n\n/**\n * Overlay optimistic \"sending\" / \"sent\" / \"failed\" messages onto the\n * server-fetched threads so a just-sent email appears immediately and is then\n * replaced by the real record once the delivery worker links it.\n *\n * Reconciliation is keyed on `messageId`: when a server thread already contains a\n * message with the same `messageId` as an optimistic one, the optimistic copy is\n * dropped (the server record wins) so the message never appears twice. Ordering\n * matches the server build: messages ascending by `sentAt`, threads descending by\n * `lastMessageAt`. The input arrays are never mutated.\n */\nexport function mergeOptimisticEmailThreads(\n serverThreads: EmailThread[],\n optimistic: OptimisticEmailMessage[],\n): EmailThread[] {\n if (optimistic.length === 0) return serverThreads\n\n const serverMessageIds = new Set<string>()\n for (const thread of serverThreads) {\n for (const message of thread.messages) {\n if (message.messageId) serverMessageIds.add(message.messageId)\n }\n }\n\n // Drop optimistic copies the server has already linked (dedupe by messageId).\n const pending = optimistic.filter(\n (entry) => !(entry.messageId && serverMessageIds.has(entry.messageId)),\n )\n if (pending.length === 0) return serverThreads\n\n const byKey = new Map<string, EmailThread>()\n const order: string[] = []\n for (const thread of serverThreads) {\n byKey.set(thread.threadKey, thread)\n order.push(thread.threadKey)\n }\n\n for (const entry of pending) {\n const { threadKey, ...message } = entry\n const existing = byKey.get(threadKey)\n if (existing) {\n // Guard against double-adding the same optimistic message across re-renders.\n if (existing.messages.some((m) => m.id === message.id)) continue\n const messages = [...existing.messages, message].sort((a, b) =>\n a.sentAt.localeCompare(b.sentAt),\n )\n const last = messages[messages.length - 1]\n byKey.set(threadKey, {\n ...existing,\n messages,\n messageCount: messages.length,\n lastMessageAt: last?.sentAt ?? existing.lastMessageAt,\n lastDirection: last?.direction ?? existing.lastDirection,\n })\n } else {\n byKey.set(threadKey, {\n threadKey,\n subject: message.subject,\n preview: message.bodyText ? message.bodyText.slice(0, 140) : null,\n participants: message.to,\n lastMessageAt: message.sentAt,\n messageCount: 1,\n providerKey: message.providerKey,\n lastDirection: message.direction,\n messages: [message],\n })\n order.push(threadKey)\n }\n }\n\n return order\n .map((key) => byKey.get(key)!)\n .sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt))\n}\n"],
|
|
5
|
+
"mappings": "AAqBO,SAAS,4BACd,eACA,YACe;AACf,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,QAAM,mBAAmB,oBAAI,IAAY;AACzC,aAAW,UAAU,eAAe;AAClC,eAAW,WAAW,OAAO,UAAU;AACrC,UAAI,QAAQ,UAAW,kBAAiB,IAAI,QAAQ,SAAS;AAAA,IAC/D;AAAA,EACF;AAGA,QAAM,UAAU,WAAW;AAAA,IACzB,CAAC,UAAU,EAAE,MAAM,aAAa,iBAAiB,IAAI,MAAM,SAAS;AAAA,EACtE;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,QAAQ,oBAAI,IAAyB;AAC3C,QAAM,QAAkB,CAAC;AACzB,aAAW,UAAU,eAAe;AAClC,UAAM,IAAI,OAAO,WAAW,MAAM;AAClC,UAAM,KAAK,OAAO,SAAS;AAAA,EAC7B;AAEA,aAAW,SAAS,SAAS;AAC3B,UAAM,EAAE,WAAW,GAAG,QAAQ,IAAI;AAClC,UAAM,WAAW,MAAM,IAAI,SAAS;AACpC,QAAI,UAAU;AAEZ,UAAI,SAAS,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,EAAE,EAAG;AACxD,YAAM,WAAW,CAAC,GAAG,SAAS,UAAU,OAAO,EAAE;AAAA,QAAK,CAAC,GAAG,MACxD,EAAE,OAAO,cAAc,EAAE,MAAM;AAAA,MACjC;AACA,YAAM,OAAO,SAAS,SAAS,SAAS,CAAC;AACzC,YAAM,IAAI,WAAW;AAAA,QACnB,GAAG;AAAA,QACH;AAAA,QACA,cAAc,SAAS;AAAA,QACvB,eAAe,MAAM,UAAU,SAAS;AAAA,QACxC,eAAe,MAAM,aAAa,SAAS;AAAA,MAC7C,CAAC;AAAA,IACH,OAAO;AACL,YAAM,IAAI,WAAW;AAAA,QACnB;AAAA,QACA,SAAS,QAAQ;AAAA,QACjB,SAAS,QAAQ,WAAW,QAAQ,SAAS,MAAM,GAAG,GAAG,IAAI;AAAA,QAC7D,cAAc,QAAQ;AAAA,QACtB,eAAe,QAAQ;AAAA,QACvB,cAAc;AAAA,QACd,aAAa,QAAQ;AAAA,QACrB,eAAe,QAAQ;AAAA,QACvB,UAAU,CAAC,OAAO;AAAA,MACpB,CAAC;AACD,YAAM,KAAK,SAAS;AAAA,IACtB;AAAA,EACF;AAEA,SAAO,MACJ,IAAI,CAAC,QAAQ,MAAM,IAAI,GAAG,CAAE,EAC5B,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,cAAc,EAAE,aAAa,CAAC;AAClE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -29,6 +29,7 @@ function buildHandlerContext(runtime) {
|
|
|
29
29
|
userId: runtime.userId,
|
|
30
30
|
features: runtime.features,
|
|
31
31
|
currentPath: runtime.currentPath,
|
|
32
|
+
t: runtime.t,
|
|
32
33
|
toast(options) {
|
|
33
34
|
const text = options.body ? `${options.title}: ${options.body}` : options.title;
|
|
34
35
|
flash(text, toFlashKind(options.severity));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/notifications/NotificationDispatcher.ts"],
|
|
4
|
-
"sourcesContent": ["import { flash, type FlashKind } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n getNotificationHandlerEntries,\n} from '@open-mercato/shared/lib/notifications/handler-registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type {\n NotificationHandler,\n NotificationHandlerContext,\n NotificationHandlerPopupOptions,\n NotificationHandlerToastOptions,\n} from '@open-mercato/shared/modules/notifications/handler'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\n\ntype RuntimeContext = {\n userId?: string\n features: string[]\n currentPath: string\n refreshNotifications: () => void\n navigate: (href: string) => void\n markAsRead: (notificationId: string) => Promise<void>\n dismiss: (notificationId: string) => Promise<void>\n}\n\ntype NotificationEffectListener = {\n id: number\n pattern: string | string[]\n effect: (notification: NotificationDto) => void\n}\n\nconst DEFAULT_PRIORITY = 50\nconst MAX_TRACKED_IDS = 500\nconst RETAINED_IDS_AFTER_PRUNE = 200\nconst POPUP_EVENT_NAME = 'om:notifications:popup'\n\nfunction toFlashKind(value: NotificationHandlerToastOptions['severity'] | NotificationHandlerPopupOptions['severity']): FlashKind {\n if (value === 'success') return 'success'\n if (value === 'warning') return 'warning'\n if (value === 'error') return 'error'\n return 'info'\n}\n\nfunction matchesType(pattern: string | string[], type: string): boolean {\n const patterns = Array.isArray(pattern) ? pattern : [pattern]\n return patterns.some((current) => {\n if (current === '*') return true\n if (current.endsWith('.*')) return type.startsWith(current.slice(0, -1))\n return current === type\n })\n}\n\nfunction matchesFeatures(required: string[] | undefined, current: string[]): boolean {\n return hasAllFeatures(current, required)\n}\n\nfunction buildHandlerContext(runtime: RuntimeContext): NotificationHandlerContext {\n return {\n userId: runtime.userId,\n features: runtime.features,\n currentPath: runtime.currentPath,\n toast(options) {\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n popup(options) {\n if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {\n window.dispatchEvent(new CustomEvent(POPUP_EVENT_NAME, { detail: options }))\n }\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n emitEvent(eventName, detail) {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(eventName, { detail }))\n },\n refreshNotifications: runtime.refreshNotifications,\n navigate(href) {\n runtime.navigate(href)\n },\n markAsRead: runtime.markAsRead,\n dismiss: runtime.dismiss,\n }\n}\n\nclass NotificationDispatcher {\n private handledIds = new Set<string>()\n private handledOrder: string[] = []\n private listeners = new Map<number, NotificationEffectListener>()\n private listenerIdSeq = 1\n private debounceByHandler = new Map<string, number>()\n\n dispatch(notifications: NotificationDto[], runtime: RuntimeContext) {\n const entries = getNotificationHandlerEntries()\n if (notifications.length === 0 && this.listeners.size === 0) return\n const context = buildHandlerContext(runtime)\n const sortedEntries = entries\n .slice()\n .sort((a, b) => (b.handler.priority ?? DEFAULT_PRIORITY) - (a.handler.priority ?? DEFAULT_PRIORITY))\n\n for (const notification of notifications) {\n if (this.handledIds.has(notification.id)) continue\n\n let handlerRan = false\n let hadFeatureBlockedHandler = false\n\n for (const entry of sortedEntries) {\n const handler = entry.handler\n if (!matchesType(handler.notificationType, notification.type)) continue\n if (!matchesFeatures(handler.features, runtime.features)) {\n hadFeatureBlockedHandler = true\n continue\n }\n if (this.shouldDebounce(handler, notification.id)) continue\n handlerRan = true\n try {\n void Promise.resolve(handler.handle(notification, context))\n } catch (error) {\n console.error(`[notifications] handler failed: ${handler.id}`, error)\n }\n }\n\n for (const listener of this.listeners.values()) {\n if (!matchesType(listener.pattern, notification.type)) continue\n handlerRan = true\n try {\n listener.effect(notification)\n } catch (error) {\n console.error('[notifications] effect listener failed', error)\n }\n }\n\n // Only mark as permanently handled when something actually ran, or when no handler\n // was blocked by missing features. This prevents a race where the initial notification\n // fetch completes before the feature-check API resolves (features: []), causing all\n // feature-gated handlers to be skipped \u2014 but the notification gets permanently\n // deduped so it never fires when features are available on the next dispatch.\n if (handlerRan || !hadFeatureBlockedHandler) {\n this.markHandled(notification.id)\n }\n }\n }\n\n subscribe(pattern: string | string[], effect: (notification: NotificationDto) => void): () => void {\n const id = this.listenerIdSeq++\n this.listeners.set(id, { id, pattern, effect })\n return () => {\n this.listeners.delete(id)\n }\n }\n\n resetForTests() {\n this.handledIds.clear()\n this.handledOrder = []\n this.listeners.clear()\n this.listenerIdSeq = 1\n this.debounceByHandler.clear()\n }\n\n private shouldDebounce(handler: NotificationHandler, notificationId: string): boolean {\n if (!handler.debounceMs || handler.debounceMs <= 0) return false\n const key = `${handler.id}:${notificationId}`\n const now = Date.now()\n const previous = this.debounceByHandler.get(key)\n if (typeof previous === 'number' && now - previous < handler.debounceMs) {\n return true\n }\n this.debounceByHandler.set(key, now)\n return false\n }\n\n private markHandled(notificationId: string) {\n this.handledIds.add(notificationId)\n this.handledOrder.push(notificationId)\n if (this.handledOrder.length <= MAX_TRACKED_IDS) return\n const removeCount = this.handledOrder.length - RETAINED_IDS_AFTER_PRUNE\n const removed = this.handledOrder.splice(0, removeCount)\n for (const id of removed) {\n this.handledIds.delete(id)\n }\n }\n}\n\nconst dispatcher = new NotificationDispatcher()\n\nexport function dispatchNotificationHandlers(\n notifications: NotificationDto[],\n runtime: RuntimeContext,\n) {\n dispatcher.dispatch(notifications, runtime)\n}\n\nexport function subscribeNotificationEffects(\n pattern: string | string[],\n effect: (notification: NotificationDto) => void,\n): () => void {\n return dispatcher.subscribe(pattern, effect)\n}\n\nexport function getRequiredNotificationHandlerFeatures(): string[] {\n const required = new Set<string>()\n const entries = getNotificationHandlerEntries()\n for (const entry of entries) {\n for (const feature of entry.handler.features ?? []) {\n if (feature && feature.trim().length > 0) {\n required.add(feature)\n }\n }\n }\n return Array.from(required)\n}\n\nexport function __resetNotificationDispatcherForTests() {\n dispatcher.resetForTests()\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,aAA6B;AACtC;AAAA,EACE;AAAA,OACK;AACP,SAAS,sBAAsB;
|
|
4
|
+
"sourcesContent": ["import { flash, type FlashKind } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n getNotificationHandlerEntries,\n} from '@open-mercato/shared/lib/notifications/handler-registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type {\n NotificationHandler,\n NotificationHandlerContext,\n NotificationHandlerPopupOptions,\n NotificationHandlerToastOptions,\n} from '@open-mercato/shared/modules/notifications/handler'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\n\ntype RuntimeContext = {\n userId?: string\n features: string[]\n currentPath: string\n t?: (key: string, fallback?: string) => string\n refreshNotifications: () => void\n navigate: (href: string) => void\n markAsRead: (notificationId: string) => Promise<void>\n dismiss: (notificationId: string) => Promise<void>\n}\n\ntype NotificationEffectListener = {\n id: number\n pattern: string | string[]\n effect: (notification: NotificationDto) => void\n}\n\nconst DEFAULT_PRIORITY = 50\nconst MAX_TRACKED_IDS = 500\nconst RETAINED_IDS_AFTER_PRUNE = 200\nconst POPUP_EVENT_NAME = 'om:notifications:popup'\n\nfunction toFlashKind(value: NotificationHandlerToastOptions['severity'] | NotificationHandlerPopupOptions['severity']): FlashKind {\n if (value === 'success') return 'success'\n if (value === 'warning') return 'warning'\n if (value === 'error') return 'error'\n return 'info'\n}\n\nfunction matchesType(pattern: string | string[], type: string): boolean {\n const patterns = Array.isArray(pattern) ? pattern : [pattern]\n return patterns.some((current) => {\n if (current === '*') return true\n if (current.endsWith('.*')) return type.startsWith(current.slice(0, -1))\n return current === type\n })\n}\n\nfunction matchesFeatures(required: string[] | undefined, current: string[]): boolean {\n return hasAllFeatures(current, required)\n}\n\nfunction buildHandlerContext(runtime: RuntimeContext): NotificationHandlerContext {\n return {\n userId: runtime.userId,\n features: runtime.features,\n currentPath: runtime.currentPath,\n t: runtime.t,\n toast(options) {\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n popup(options) {\n if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {\n window.dispatchEvent(new CustomEvent(POPUP_EVENT_NAME, { detail: options }))\n }\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n emitEvent(eventName, detail) {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(eventName, { detail }))\n },\n refreshNotifications: runtime.refreshNotifications,\n navigate(href) {\n runtime.navigate(href)\n },\n markAsRead: runtime.markAsRead,\n dismiss: runtime.dismiss,\n }\n}\n\nclass NotificationDispatcher {\n private handledIds = new Set<string>()\n private handledOrder: string[] = []\n private listeners = new Map<number, NotificationEffectListener>()\n private listenerIdSeq = 1\n private debounceByHandler = new Map<string, number>()\n\n dispatch(notifications: NotificationDto[], runtime: RuntimeContext) {\n const entries = getNotificationHandlerEntries()\n if (notifications.length === 0 && this.listeners.size === 0) return\n const context = buildHandlerContext(runtime)\n const sortedEntries = entries\n .slice()\n .sort((a, b) => (b.handler.priority ?? DEFAULT_PRIORITY) - (a.handler.priority ?? DEFAULT_PRIORITY))\n\n for (const notification of notifications) {\n if (this.handledIds.has(notification.id)) continue\n\n let handlerRan = false\n let hadFeatureBlockedHandler = false\n\n for (const entry of sortedEntries) {\n const handler = entry.handler\n if (!matchesType(handler.notificationType, notification.type)) continue\n if (!matchesFeatures(handler.features, runtime.features)) {\n hadFeatureBlockedHandler = true\n continue\n }\n if (this.shouldDebounce(handler, notification.id)) continue\n handlerRan = true\n try {\n void Promise.resolve(handler.handle(notification, context))\n } catch (error) {\n console.error(`[notifications] handler failed: ${handler.id}`, error)\n }\n }\n\n for (const listener of this.listeners.values()) {\n if (!matchesType(listener.pattern, notification.type)) continue\n handlerRan = true\n try {\n listener.effect(notification)\n } catch (error) {\n console.error('[notifications] effect listener failed', error)\n }\n }\n\n // Only mark as permanently handled when something actually ran, or when no handler\n // was blocked by missing features. This prevents a race where the initial notification\n // fetch completes before the feature-check API resolves (features: []), causing all\n // feature-gated handlers to be skipped \u2014 but the notification gets permanently\n // deduped so it never fires when features are available on the next dispatch.\n if (handlerRan || !hadFeatureBlockedHandler) {\n this.markHandled(notification.id)\n }\n }\n }\n\n subscribe(pattern: string | string[], effect: (notification: NotificationDto) => void): () => void {\n const id = this.listenerIdSeq++\n this.listeners.set(id, { id, pattern, effect })\n return () => {\n this.listeners.delete(id)\n }\n }\n\n resetForTests() {\n this.handledIds.clear()\n this.handledOrder = []\n this.listeners.clear()\n this.listenerIdSeq = 1\n this.debounceByHandler.clear()\n }\n\n private shouldDebounce(handler: NotificationHandler, notificationId: string): boolean {\n if (!handler.debounceMs || handler.debounceMs <= 0) return false\n const key = `${handler.id}:${notificationId}`\n const now = Date.now()\n const previous = this.debounceByHandler.get(key)\n if (typeof previous === 'number' && now - previous < handler.debounceMs) {\n return true\n }\n this.debounceByHandler.set(key, now)\n return false\n }\n\n private markHandled(notificationId: string) {\n this.handledIds.add(notificationId)\n this.handledOrder.push(notificationId)\n if (this.handledOrder.length <= MAX_TRACKED_IDS) return\n const removeCount = this.handledOrder.length - RETAINED_IDS_AFTER_PRUNE\n const removed = this.handledOrder.splice(0, removeCount)\n for (const id of removed) {\n this.handledIds.delete(id)\n }\n }\n}\n\nconst dispatcher = new NotificationDispatcher()\n\nexport function dispatchNotificationHandlers(\n notifications: NotificationDto[],\n runtime: RuntimeContext,\n) {\n dispatcher.dispatch(notifications, runtime)\n}\n\nexport function subscribeNotificationEffects(\n pattern: string | string[],\n effect: (notification: NotificationDto) => void,\n): () => void {\n return dispatcher.subscribe(pattern, effect)\n}\n\nexport function getRequiredNotificationHandlerFeatures(): string[] {\n const required = new Set<string>()\n const entries = getNotificationHandlerEntries()\n for (const entry of entries) {\n for (const feature of entry.handler.features ?? []) {\n if (feature && feature.trim().length > 0) {\n required.add(feature)\n }\n }\n }\n return Array.from(required)\n}\n\nexport function __resetNotificationDispatcherForTests() {\n dispatcher.resetForTests()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
subscribeNotificationNew,
|
|
6
6
|
emitNotificationCountChanged
|
|
7
7
|
} from "@open-mercato/shared/lib/frontend/notificationEvents";
|
|
8
|
+
import { useOptionalT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
9
|
import {
|
|
9
10
|
dispatchNotificationHandlers,
|
|
10
11
|
getRequiredNotificationHandlerFeatures
|
|
@@ -18,6 +19,11 @@ function useNotificationsPoll() {
|
|
|
18
19
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
19
20
|
const [error, setError] = React.useState(null);
|
|
20
21
|
const grantedFeaturesRef = React.useRef([]);
|
|
22
|
+
const translate = useOptionalT();
|
|
23
|
+
const translateRef = React.useRef(translate);
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
translateRef.current = translate;
|
|
26
|
+
}, [translate]);
|
|
21
27
|
const lastIdRef = React.useRef(null);
|
|
22
28
|
const prevUnreadRef = React.useRef(0);
|
|
23
29
|
const {
|
|
@@ -52,6 +58,7 @@ function useNotificationsPoll() {
|
|
|
52
58
|
if (newNotifications.length > 0) {
|
|
53
59
|
dispatchNotificationHandlers(newNotifications, {
|
|
54
60
|
features: grantedFeaturesRef.current,
|
|
61
|
+
t: translateRef.current,
|
|
55
62
|
currentPath: typeof window === "undefined" ? "/" : `${window.location.pathname}${window.location.search}`,
|
|
56
63
|
refreshNotifications: () => {
|
|
57
64
|
void fetchNotifications();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/notifications/useNotificationsPoll.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall } from '../utils/apiCall'\nimport {\n subscribeNotificationNew,\n emitNotificationCountChanged,\n} from '@open-mercato/shared/lib/frontend/notificationEvents'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport {\n dispatchNotificationHandlers,\n getRequiredNotificationHandlerFeatures,\n} from './NotificationDispatcher'\nimport { useNotificationActions } from './useNotificationActions'\n\nexport type UseNotificationsPollResult = {\n notifications: NotificationDto[]\n unreadCount: number\n hasNew: boolean\n isLoading: boolean\n error: string | null\n refresh: () => void\n markAsRead: (id: string) => Promise<void>\n executeAction: (id: string, actionId: string) => Promise<{ href?: string }>\n dismiss: (id: string) => Promise<void>\n dismissUndo: { notification: NotificationDto; previousStatus: 'read' | 'unread' } | null\n undoDismiss: () => Promise<void>\n markAllRead: () => Promise<void>\n}\n\nconst POLL_INTERVAL = 5000\n\nexport function useNotificationsPoll(): UseNotificationsPollResult {\n const [notifications, setNotifications] = React.useState<NotificationDto[]>([])\n const [unreadCount, setUnreadCount] = React.useState(0)\n const [hasNew, setHasNew] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const grantedFeaturesRef = React.useRef<string[]>([])\n const lastIdRef = React.useRef<string | null>(null)\n const prevUnreadRef = React.useRef(0)\n const {\n markAsRead,\n executeAction,\n dismiss,\n dismissUndo,\n undoDismiss,\n markAllRead,\n markAsReadRef,\n dismissRef,\n } = useNotificationActions(notifications, setNotifications, setUnreadCount)\n\n const fetchNotifications = React.useCallback(async () => {\n try {\n const [notifResult, countResult] = await Promise.all([\n apiCall<{ items: NotificationDto[] }>('/api/notifications?pageSize=50'),\n apiCall<{ unreadCount: number }>('/api/notifications/unread-count'),\n ])\n\n if (notifResult.ok && notifResult.result) {\n const newNotifications = notifResult.result.items\n\n if (lastIdRef.current && newNotifications.length > 0) {\n const firstId = newNotifications[0].id\n if (firstId !== lastIdRef.current) {\n setHasNew(true)\n setTimeout(() => setHasNew(false), 3000)\n }\n }\n\n if (newNotifications.length > 0) {\n lastIdRef.current = newNotifications[0].id\n }\n\n setNotifications(newNotifications)\n\n if (newNotifications.length > 0) {\n dispatchNotificationHandlers(newNotifications, {\n features: grantedFeaturesRef.current,\n currentPath:\n typeof window === 'undefined'\n ? '/'\n : `${window.location.pathname}${window.location.search}`,\n refreshNotifications: () => {\n void fetchNotifications()\n },\n navigate: (href) => {\n if (typeof window === 'undefined') return\n if (!href.startsWith('/')) return\n window.location.assign(href)\n },\n markAsRead: async (notificationId) => markAsReadRef.current(notificationId),\n dismiss: async (notificationId) => dismissRef.current(notificationId),\n })\n }\n }\n\n if (countResult.ok && countResult.result) {\n const newCount = countResult.result.unreadCount\n if (newCount !== prevUnreadRef.current) {\n setUnreadCount(newCount)\n prevUnreadRef.current = newCount\n emitNotificationCountChanged(newCount)\n }\n }\n\n setError(null)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch notifications')\n } finally {\n setIsLoading(false)\n }\n }, [])\n\n const refresh = React.useCallback(() => {\n fetchNotifications()\n }, [fetchNotifications])\n\n React.useEffect(() => {\n let mounted = true\n const run = async () => {\n const requiredFeatures = getRequiredNotificationHandlerFeatures()\n if (requiredFeatures.length === 0) {\n grantedFeaturesRef.current = []\n return\n }\n const response = await apiCall<{ granted?: string[] }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: requiredFeatures }),\n })\n if (!mounted) return\n grantedFeaturesRef.current = response.ok\n ? (response.result?.granted ?? [])\n : []\n }\n void run()\n return () => {\n mounted = false\n }\n }, [])\n\n React.useEffect(() => {\n fetchNotifications()\n const interval = setInterval(fetchNotifications, POLL_INTERVAL)\n return () => clearInterval(interval)\n }, [fetchNotifications])\n\n React.useEffect(() => {\n const unsub = subscribeNotificationNew(() => refresh())\n return unsub\n }, [refresh])\n\n return {\n notifications,\n unreadCount,\n hasNew,\n isLoading,\n error,\n refresh,\n markAsRead,\n executeAction,\n dismiss,\n dismissUndo,\n undoDismiss,\n markAllRead,\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall } from '../utils/apiCall'\nimport {\n subscribeNotificationNew,\n emitNotificationCountChanged,\n} from '@open-mercato/shared/lib/frontend/notificationEvents'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport { useOptionalT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n dispatchNotificationHandlers,\n getRequiredNotificationHandlerFeatures,\n} from './NotificationDispatcher'\nimport { useNotificationActions } from './useNotificationActions'\n\nexport type UseNotificationsPollResult = {\n notifications: NotificationDto[]\n unreadCount: number\n hasNew: boolean\n isLoading: boolean\n error: string | null\n refresh: () => void\n markAsRead: (id: string) => Promise<void>\n executeAction: (id: string, actionId: string) => Promise<{ href?: string }>\n dismiss: (id: string) => Promise<void>\n dismissUndo: { notification: NotificationDto; previousStatus: 'read' | 'unread' } | null\n undoDismiss: () => Promise<void>\n markAllRead: () => Promise<void>\n}\n\nconst POLL_INTERVAL = 5000\n\nexport function useNotificationsPoll(): UseNotificationsPollResult {\n const [notifications, setNotifications] = React.useState<NotificationDto[]>([])\n const [unreadCount, setUnreadCount] = React.useState(0)\n const [hasNew, setHasNew] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const grantedFeaturesRef = React.useRef<string[]>([])\n const translate = useOptionalT()\n const translateRef = React.useRef(translate)\n React.useEffect(() => { translateRef.current = translate }, [translate])\n const lastIdRef = React.useRef<string | null>(null)\n const prevUnreadRef = React.useRef(0)\n const {\n markAsRead,\n executeAction,\n dismiss,\n dismissUndo,\n undoDismiss,\n markAllRead,\n markAsReadRef,\n dismissRef,\n } = useNotificationActions(notifications, setNotifications, setUnreadCount)\n\n const fetchNotifications = React.useCallback(async () => {\n try {\n const [notifResult, countResult] = await Promise.all([\n apiCall<{ items: NotificationDto[] }>('/api/notifications?pageSize=50'),\n apiCall<{ unreadCount: number }>('/api/notifications/unread-count'),\n ])\n\n if (notifResult.ok && notifResult.result) {\n const newNotifications = notifResult.result.items\n\n if (lastIdRef.current && newNotifications.length > 0) {\n const firstId = newNotifications[0].id\n if (firstId !== lastIdRef.current) {\n setHasNew(true)\n setTimeout(() => setHasNew(false), 3000)\n }\n }\n\n if (newNotifications.length > 0) {\n lastIdRef.current = newNotifications[0].id\n }\n\n setNotifications(newNotifications)\n\n if (newNotifications.length > 0) {\n dispatchNotificationHandlers(newNotifications, {\n features: grantedFeaturesRef.current,\n t: translateRef.current,\n currentPath:\n typeof window === 'undefined'\n ? '/'\n : `${window.location.pathname}${window.location.search}`,\n refreshNotifications: () => {\n void fetchNotifications()\n },\n navigate: (href) => {\n if (typeof window === 'undefined') return\n if (!href.startsWith('/')) return\n window.location.assign(href)\n },\n markAsRead: async (notificationId) => markAsReadRef.current(notificationId),\n dismiss: async (notificationId) => dismissRef.current(notificationId),\n })\n }\n }\n\n if (countResult.ok && countResult.result) {\n const newCount = countResult.result.unreadCount\n if (newCount !== prevUnreadRef.current) {\n setUnreadCount(newCount)\n prevUnreadRef.current = newCount\n emitNotificationCountChanged(newCount)\n }\n }\n\n setError(null)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch notifications')\n } finally {\n setIsLoading(false)\n }\n }, [])\n\n const refresh = React.useCallback(() => {\n fetchNotifications()\n }, [fetchNotifications])\n\n React.useEffect(() => {\n let mounted = true\n const run = async () => {\n const requiredFeatures = getRequiredNotificationHandlerFeatures()\n if (requiredFeatures.length === 0) {\n grantedFeaturesRef.current = []\n return\n }\n const response = await apiCall<{ granted?: string[] }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: requiredFeatures }),\n })\n if (!mounted) return\n grantedFeaturesRef.current = response.ok\n ? (response.result?.granted ?? [])\n : []\n }\n void run()\n return () => {\n mounted = false\n }\n }, [])\n\n React.useEffect(() => {\n fetchNotifications()\n const interval = setInterval(fetchNotifications, POLL_INTERVAL)\n return () => clearInterval(interval)\n }, [fetchNotifications])\n\n React.useEffect(() => {\n const unsub = subscribeNotificationNew(() => refresh())\n return unsub\n }, [refresh])\n\n return {\n notifications,\n unreadCount,\n hasNew,\n isLoading,\n error,\n refresh,\n markAsRead,\n executeAction,\n dismiss,\n dismissUndo,\n undoDismiss,\n markAllRead,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|