@open-mercato/core 0.10.1-develop.7771.1.615a6c9ba9 → 0.10.1-develop.7772.1.9d3d6ce261
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 +1 -1
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +6 -7
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
- package/dist/modules/communication_channels/data/validators.js +2 -1
- package/dist/modules/communication_channels/data/validators.js.map +2 -2
- package/dist/modules/communication_channels/lib/email-mime.js +13 -3
- package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
- package/dist/modules/communication_channels/lib/thread-matcher.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +16 -6
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/lib/interactionOrderBy.js +12 -0
- package/dist/modules/customers/lib/interactionOrderBy.js.map +7 -0
- package/dist/modules/customers/lib/link-channel-message-handler.js +12 -11
- package/dist/modules/customers/lib/link-channel-message-handler.js.map +2 -2
- package/dist/modules/customers/lib/personEmailThreads.js +43 -12
- package/dist/modules/customers/lib/personEmailThreads.js.map +3 -3
- package/dist/modules/messages/api/route.js +2 -1
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -1
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +10 -8
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js +1 -1
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js +3 -3
- package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js +1 -1
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
- package/dist/modules/messages/components/message-detail/utils.js +3 -3
- package/dist/modules/messages/components/message-detail/utils.js.map +2 -2
- package/dist/modules/messages/data/validators.js +3 -2
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/messages/frontend/messages/view/[token]/page.js +6 -5
- package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
- package/dist/modules/messages/lib/forwarding.js +3 -1
- package/dist/modules/messages/lib/forwarding.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +5 -7
- package/src/modules/communication_channels/data/validators.ts +2 -1
- package/src/modules/communication_channels/lib/adapter.ts +4 -1
- package/src/modules/communication_channels/lib/email-mime.ts +24 -7
- package/src/modules/communication_channels/lib/thread-matcher.ts +2 -1
- package/src/modules/customers/api/activities/route.ts +17 -7
- package/src/modules/customers/lib/interactionOrderBy.ts +9 -0
- package/src/modules/customers/lib/link-channel-message-handler.ts +27 -38
- package/src/modules/customers/lib/personEmailThreads.ts +63 -21
- package/src/modules/messages/api/route.ts +3 -1
- package/src/modules/messages/commands/messages.ts +2 -1
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +2 -0
- package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +10 -8
- package/src/modules/messages/components/message-detail/panels/MessageHeader.tsx +3 -1
- package/src/modules/messages/components/message-detail/panels/MessageListComponent.tsx +5 -3
- package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +1 -1
- package/src/modules/messages/components/message-detail/utils.ts +3 -3
- package/src/modules/messages/data/validators.ts +4 -3
- package/src/modules/messages/frontend/messages/view/[token]/page.tsx +5 -4
- package/src/modules/messages/i18n/de.json +1 -0
- package/src/modules/messages/i18n/en.json +1 -0
- package/src/modules/messages/i18n/es.json +1 -0
- package/src/modules/messages/i18n/ko.json +1 -0
- package/src/modules/messages/i18n/pl.json +2 -1
- package/src/modules/messages/lib/forwarding.ts +3 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/components/MessagesInboxPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/messages/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { useAppEventCoalesced } from '@open-mercato/ui/backend/injection/useAppEventCoalesced'\nimport { Archive, ChevronDown, FilePenLine, Inbox, Layers, Send } from 'lucide-react'\nimport { getMessageUiComponentRegistry } from './utils/typeUiRegistry'\nimport { DefaultMessageListItem } from './defaults/DefaultMessageListItem'\nimport { getMessageListParticipantLabel } from './messageListLabels'\nimport { toErrorMessage } from './message-detail/utils'\nimport { useMessagesInboxBulkActions, type MessageFolder } from './useMessagesInboxBulkActions'\nimport {\n buildMessagesInboxFilters,\n buildMessagesListParams,\n buildSenderOptionsFromMessages,\n type SenderOption,\n} from './inboxFilters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'MessagesInboxPageClient' })\n\ntype MessageListItem = {\n id: string\n type: string\n subject: string\n bodyPreview: string\n senderUserId: string\n senderName?: string | null\n senderEmail?: string | null\n externalName?: string | null\n externalEmail?: string | null\n sourceEntityType?: string | null\n priority: string\n status: string\n hasObjects: boolean\n objectCount: number\n hasAttachments: boolean\n attachmentCount: number\n recipientCount?: number\n hasActions: boolean\n actionTaken?: string | null\n sentAt?: string | null\n readAt?: string | null\n threadId?: string | null\n}\n\ntype MessageListResponse = {\n items?: MessageListItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\ntype MessageTypeItem = {\n type: string\n module: string\n labelKey: string\n ui?: {\n listItemComponent?: string | null\n } | null\n}\n\ntype UserListItem = {\n id: string\n email?: string | null\n name?: string | null\n}\n\nexport function MessagesInboxPageClient() {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n\n const invalidateMessageListQueries = React.useCallback(() => {\n void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n }, [queryClient])\n\n useAppEventCoalesced('messages.message.*', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n // Reconnect means events were missed while offline, so it refreshes uncoalesced.\n useAppEvent('om:bridge:reconnected', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n const [folder, setFolder] = React.useState<MessageFolder>('inbox')\n const [folderMenuOpen, setFolderMenuOpen] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [page, setPage] = React.useState(1)\n const pageSize = 20\n const folderMenuRef = React.useRef<HTMLDivElement | null>(null)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const { bulkActions, selectionScopeKey, injectionContext, ConfirmDialogElement } = useMessagesInboxBulkActions<MessageListItem>({\n folder,\n page,\n search,\n filterValues,\n })\n\n const listQuery = useQuery({\n queryKey: [\n 'messages',\n 'list',\n folder,\n search,\n page,\n pageSize,\n JSON.stringify(filterValues),\n scopeVersion,\n ],\n queryFn: async () => {\n const params = buildMessagesListParams({\n folder,\n page,\n pageSize,\n search,\n filterValues,\n })\n\n const call = await apiCall<MessageListResponse>(`/api/messages?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadListFailed', 'Failed to load messages.'),\n )\n }\n\n return {\n items: Array.isArray(call.result?.items) ? call.result?.items ?? [] : [],\n total: Number(call.result?.total ?? 0),\n page: Number(call.result?.page ?? page),\n pageSize: Number(call.result?.pageSize ?? pageSize),\n totalPages: Number(call.result?.totalPages ?? 0),\n totalIsCapped: call.result?.totalIsCapped === true,\n }\n },\n })\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types', scopeVersion],\n queryFn: async () => {\n const call = await apiCall<{ items?: MessageTypeItem[] }>('/api/messages/types')\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n )\n }\n return Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n },\n })\n\n React.useEffect(() => {\n if (!listQuery.error) return\n flash(\n listQuery.error instanceof Error\n ? listQuery.error.message\n : t('messages.errors.loadListFailed', 'Failed to load messages.'),\n 'error',\n )\n }, [listQuery.error, t])\n\n React.useEffect(() => {\n if (!messageTypesQuery.error) return\n flash(\n messageTypesQuery.error instanceof Error\n ? messageTypesQuery.error.message\n : t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n 'error',\n )\n }, [messageTypesQuery.error, t])\n\n const messageTypeLabelMap = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = t(item.labelKey, item.type)\n }\n return map\n }, [messageTypesQuery.data, t])\n\n const [senderOptions, setSenderOptions] = React.useState<SenderOption[]>([])\n const senderOptionsScopeRef = React.useRef(scopeVersion)\n\n const mergeSenderOptions = React.useCallback((incoming: SenderOption[]) => {\n if (incoming.length === 0) return\n setSenderOptions((prev) => {\n const map = new Map<string, SenderOption>()\n for (const opt of prev) map.set(opt.value, opt)\n for (const opt of incoming) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n }, [])\n\n const loadSenderOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) {\n params.set('search', query.trim())\n }\n\n const call = await apiCall<{ items?: UserListItem[] }>(\n `/api/auth/users?${params.toString()}`,\n {\n headers: {\n 'x-om-forbidden-redirect': '0',\n },\n },\n ).catch(() => null)\n if (!call) return []\n if (!call.ok) return []\n\n const items = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n const next: SenderOption[] = items.flatMap((item) => {\n if (!item || typeof item.id !== 'string' || item.id.trim().length === 0) return []\n const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{\n value: item.id,\n label,\n description: email && email !== label ? email : null,\n }]\n })\n if (senderOptionsScopeRef.current === scopeVersion) {\n mergeSenderOptions(next)\n }\n return next\n }, [mergeSenderOptions, scopeVersion])\n\n React.useEffect(() => {\n const items = listQuery.data?.items ?? []\n mergeSenderOptions(buildSenderOptionsFromMessages(items))\n }, [listQuery.data?.items, mergeSenderOptions])\n\n React.useEffect(() => {\n senderOptionsScopeRef.current = scopeVersion\n setSenderOptions([])\n loadSenderOptions().catch((error: unknown) => {\n logger.warn('Failed to load sender filter options', { err: error })\n })\n }, [loadSenderOptions, scopeVersion])\n\n const listItemComponentKeyByType = React.useMemo(() => {\n const map: Record<string, string | null> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = item.ui?.listItemComponent ?? null\n }\n return map\n }, [messageTypesQuery.data])\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const typeOptions = (messageTypesQuery.data ?? []).map((item) => ({\n value: item.type,\n label: t(item.labelKey, item.type),\n }))\n\n return buildMessagesInboxFilters({\n t,\n typeOptions,\n senderOptions,\n loadSenderOptions,\n })\n }, [loadSenderOptions, messageTypesQuery.data, senderOptions, t])\n\n const columns = React.useMemo<ColumnDef<MessageListItem>[]>(() => [\n {\n accessorKey: 'message',\n header: t('messages.title', 'Messages'),\n meta: {\n truncate: false,\n maxWidth: '100%',\n },\n cell: ({ row }) => {\n const item = row.original\n const listItemComponentKey = listItemComponentKeyByType[item.type]\n const ListItemComponent = listItemComponentKey\n ? messageUiRegistry.listItemComponents[listItemComponentKey] ?? null\n : null\n const ComponentToUse = ListItemComponent || DefaultMessageListItem\n\n return (\n <ComponentToUse\n message={{\n id: item.id,\n type: item.type,\n typeLabel: messageTypeLabelMap[item.type] ?? item.type,\n subject: item.subject,\n body: item.bodyPreview,\n bodyFormat: 'text' as const,\n priority: (item.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal',\n sentAt: item.sentAt ? new Date(item.sentAt) : null,\n senderName: getMessageListParticipantLabel(item, folder, t),\n hasObjects: item.hasObjects,\n objectCount: item.objectCount,\n hasAttachments: item.hasAttachments,\n attachmentCount: item.attachmentCount,\n recipientCount: item.recipientCount ?? 0,\n hasActions: item.hasActions,\n actionTaken: item.actionTaken ?? null,\n unread: item.status === 'unread',\n }}\n onClick={() => router.push(`/backend/messages/${item.id}`)}\n />\n )\n },\n },\n ], [folder, listItemComponentKeyByType, messageTypeLabelMap, messageUiRegistry, router, t])\n\n const folderOptions = React.useMemo(() => [\n { id: 'inbox' as const, label: t('messages.folder.inbox', 'Inbox'), icon: Inbox },\n { id: 'sent' as const, label: t('messages.folder.sent', 'Sent'), icon: Send },\n { id: 'drafts' as const, label: t('messages.folder.drafts', 'Drafts'), icon: FilePenLine },\n { id: 'archived' as const, label: t('messages.folder.archived', 'Archived'), icon: Archive },\n { id: 'all' as const, label: t('messages.folder.all', 'All'), icon: Layers },\n ], [t])\n\n const activeFolderOption = folderOptions.find((option) => option.id === folder) ?? folderOptions[0]\n const ActiveFolderIcon = activeFolderOption.icon\n\n React.useEffect(() => {\n if (!folderMenuOpen) return\n const handleClickOutside = (event: MouseEvent) => {\n if (!folderMenuRef.current) return\n const target = event.target\n if (target instanceof Node && !folderMenuRef.current.contains(target)) {\n setFolderMenuOpen(false)\n }\n }\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setFolderMenuOpen(false)\n }\n document.addEventListener('mousedown', handleClickOutside)\n document.addEventListener('keydown', handleEscape)\n return () => {\n document.removeEventListener('mousedown', handleClickOutside)\n document.removeEventListener('keydown', handleEscape)\n }\n }, [folderMenuOpen])\n\n const rows = listQuery.data?.items ?? []\n const total = listQuery.data?.total ?? 0\n const totalIsCapped = listQuery.data?.totalIsCapped === true\n const totalPages = listQuery.data?.totalPages ?? 0\n\n return (\n <div className=\"space-y-4\">\n <DataTable\n title={t('messages.title', 'Messages')}\n titleHeadingLevel={1}\n // UMES extension surface \u2014 opt into widget injection at:\n // data-table:messages:columns / :row-actions / :bulk-actions / :filters / :toolbar / :search-trailing\n // (SPEC-045d \u00A79.3a \u2014 communication_channels hub renders channel badge + delivery status here)\n extensionTableId={extensionPoints.hosts.inboxTable.tableId}\n columns={columns}\n data={rows}\n bulkActions={bulkActions}\n selectionScopeKey={selectionScopeKey}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={t('messages.searchPlaceholder', 'Search messages')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(value) => {\n setFilterValues(value)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n injectionContext={injectionContext}\n isLoading={listQuery.isLoading || listQuery.isFetching}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n totalIsCapped,\n onPageChange: setPage,\n }}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative\" ref={folderMenuRef}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n aria-expanded={folderMenuOpen}\n aria-haspopup=\"menu\"\n onClick={() => setFolderMenuOpen((value) => !value)}\n >\n <ActiveFolderIcon className=\"h-4 w-4\" aria-hidden />\n <span>{t('messages.folder.selector', 'Folder')}:</span>\n <span>{activeFolderOption.label}</span>\n <ChevronDown className=\"h-4 w-4 opacity-70\" aria-hidden />\n </Button>\n {folderMenuOpen ? (\n <div\n className=\"absolute right-0 z-dropdown mt-1 min-w-52 rounded-md border bg-background p-1 shadow\"\n role=\"menu\"\n >\n {folderOptions.map((option) => {\n const Icon = option.icon\n const isActive = option.id === folder\n return (\n <Button\n key={option.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"menuitemradio\"\n aria-checked={isActive}\n className={`w-full justify-start h-auto px-2 py-1.5 text-sm font-normal ${isActive ? 'bg-accent/60' : ''}`}\n onClick={() => {\n setFolder(option.id)\n setPage(1)\n setFolderMenuOpen(false)\n }}\n >\n <Icon className=\"h-4 w-4\" aria-hidden />\n <span>{option.label}</span>\n </Button>\n )\n })}\n </div>\n ) : null}\n </div>\n <Button asChild>\n <Link href=\"/backend/messages/compose\">{t('messages.compose', 'Compose message')}</Link>\n </Button>\n </div>\n }\n onRowClick={(row) => {\n router.push(`/backend/messages/${row.id}`)\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('messages.title', 'Messages')}\n createHref=\"/backend/messages/compose\"\n createLabel={t('messages.compose', 'Compose message')}\n />\n )}\n embedded\n />\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/messages/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { useAppEventCoalesced } from '@open-mercato/ui/backend/injection/useAppEventCoalesced'\nimport { Archive, ChevronDown, FilePenLine, Inbox, Layers, Send } from 'lucide-react'\nimport { getMessageUiComponentRegistry } from './utils/typeUiRegistry'\nimport { DefaultMessageListItem } from './defaults/DefaultMessageListItem'\nimport { getMessageListParticipantLabel } from './messageListLabels'\nimport { toErrorMessage } from './message-detail/utils'\nimport { useMessagesInboxBulkActions, type MessageFolder } from './useMessagesInboxBulkActions'\nimport {\n buildMessagesInboxFilters,\n buildMessagesListParams,\n buildSenderOptionsFromMessages,\n type SenderOption,\n} from './inboxFilters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'MessagesInboxPageClient' })\n\ntype MessageListItem = {\n id: string\n type: string\n subject: string\n bodyPreview: string\n senderUserId: string\n senderName?: string | null\n senderEmail?: string | null\n externalName?: string | null\n externalEmail?: string | null\n sourceEntityType?: string | null\n priority: string\n status: string\n isDraft: boolean\n hasObjects: boolean\n objectCount: number\n hasAttachments: boolean\n attachmentCount: number\n recipientCount?: number\n hasActions: boolean\n actionTaken?: string | null\n sentAt?: string | null\n readAt?: string | null\n threadId?: string | null\n}\n\ntype MessageListResponse = {\n items?: MessageListItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\ntype MessageTypeItem = {\n type: string\n module: string\n labelKey: string\n ui?: {\n listItemComponent?: string | null\n } | null\n}\n\ntype UserListItem = {\n id: string\n email?: string | null\n name?: string | null\n}\n\nexport function MessagesInboxPageClient() {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n\n const invalidateMessageListQueries = React.useCallback(() => {\n void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n }, [queryClient])\n\n useAppEventCoalesced('messages.message.*', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n // Reconnect means events were missed while offline, so it refreshes uncoalesced.\n useAppEvent('om:bridge:reconnected', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n const [folder, setFolder] = React.useState<MessageFolder>('inbox')\n const [folderMenuOpen, setFolderMenuOpen] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [page, setPage] = React.useState(1)\n const pageSize = 20\n const folderMenuRef = React.useRef<HTMLDivElement | null>(null)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const { bulkActions, selectionScopeKey, injectionContext, ConfirmDialogElement } = useMessagesInboxBulkActions<MessageListItem>({\n folder,\n page,\n search,\n filterValues,\n })\n\n const listQuery = useQuery({\n queryKey: [\n 'messages',\n 'list',\n folder,\n search,\n page,\n pageSize,\n JSON.stringify(filterValues),\n scopeVersion,\n ],\n queryFn: async () => {\n const params = buildMessagesListParams({\n folder,\n page,\n pageSize,\n search,\n filterValues,\n })\n\n const call = await apiCall<MessageListResponse>(`/api/messages?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadListFailed', 'Failed to load messages.'),\n )\n }\n\n return {\n items: Array.isArray(call.result?.items) ? call.result?.items ?? [] : [],\n total: Number(call.result?.total ?? 0),\n page: Number(call.result?.page ?? page),\n pageSize: Number(call.result?.pageSize ?? pageSize),\n totalPages: Number(call.result?.totalPages ?? 0),\n totalIsCapped: call.result?.totalIsCapped === true,\n }\n },\n })\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types', scopeVersion],\n queryFn: async () => {\n const call = await apiCall<{ items?: MessageTypeItem[] }>('/api/messages/types')\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n )\n }\n return Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n },\n })\n\n React.useEffect(() => {\n if (!listQuery.error) return\n flash(\n listQuery.error instanceof Error\n ? listQuery.error.message\n : t('messages.errors.loadListFailed', 'Failed to load messages.'),\n 'error',\n )\n }, [listQuery.error, t])\n\n React.useEffect(() => {\n if (!messageTypesQuery.error) return\n flash(\n messageTypesQuery.error instanceof Error\n ? messageTypesQuery.error.message\n : t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n 'error',\n )\n }, [messageTypesQuery.error, t])\n\n const messageTypeLabelMap = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = t(item.labelKey, item.type)\n }\n return map\n }, [messageTypesQuery.data, t])\n\n const [senderOptions, setSenderOptions] = React.useState<SenderOption[]>([])\n const senderOptionsScopeRef = React.useRef(scopeVersion)\n\n const mergeSenderOptions = React.useCallback((incoming: SenderOption[]) => {\n if (incoming.length === 0) return\n setSenderOptions((prev) => {\n const map = new Map<string, SenderOption>()\n for (const opt of prev) map.set(opt.value, opt)\n for (const opt of incoming) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n }, [])\n\n const loadSenderOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) {\n params.set('search', query.trim())\n }\n\n const call = await apiCall<{ items?: UserListItem[] }>(\n `/api/auth/users?${params.toString()}`,\n {\n headers: {\n 'x-om-forbidden-redirect': '0',\n },\n },\n ).catch(() => null)\n if (!call) return []\n if (!call.ok) return []\n\n const items = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n const next: SenderOption[] = items.flatMap((item) => {\n if (!item || typeof item.id !== 'string' || item.id.trim().length === 0) return []\n const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{\n value: item.id,\n label,\n description: email && email !== label ? email : null,\n }]\n })\n if (senderOptionsScopeRef.current === scopeVersion) {\n mergeSenderOptions(next)\n }\n return next\n }, [mergeSenderOptions, scopeVersion])\n\n React.useEffect(() => {\n const items = listQuery.data?.items ?? []\n mergeSenderOptions(buildSenderOptionsFromMessages(items))\n }, [listQuery.data?.items, mergeSenderOptions])\n\n React.useEffect(() => {\n senderOptionsScopeRef.current = scopeVersion\n setSenderOptions([])\n loadSenderOptions().catch((error: unknown) => {\n logger.warn('Failed to load sender filter options', { err: error })\n })\n }, [loadSenderOptions, scopeVersion])\n\n const listItemComponentKeyByType = React.useMemo(() => {\n const map: Record<string, string | null> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = item.ui?.listItemComponent ?? null\n }\n return map\n }, [messageTypesQuery.data])\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const typeOptions = (messageTypesQuery.data ?? []).map((item) => ({\n value: item.type,\n label: t(item.labelKey, item.type),\n }))\n\n return buildMessagesInboxFilters({\n t,\n typeOptions,\n senderOptions,\n loadSenderOptions,\n })\n }, [loadSenderOptions, messageTypesQuery.data, senderOptions, t])\n\n const columns = React.useMemo<ColumnDef<MessageListItem>[]>(() => [\n {\n accessorKey: 'message',\n header: t('messages.title', 'Messages'),\n meta: {\n truncate: false,\n maxWidth: '100%',\n },\n cell: ({ row }) => {\n const item = row.original\n const listItemComponentKey = listItemComponentKeyByType[item.type]\n const ListItemComponent = listItemComponentKey\n ? messageUiRegistry.listItemComponents[listItemComponentKey] ?? null\n : null\n const ComponentToUse = ListItemComponent || DefaultMessageListItem\n\n return (\n <ComponentToUse\n message={{\n id: item.id,\n type: item.type,\n typeLabel: messageTypeLabelMap[item.type] ?? item.type,\n subject: item.subject,\n body: item.bodyPreview,\n bodyFormat: 'text' as const,\n priority: (item.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal',\n sentAt: item.sentAt ? new Date(item.sentAt) : null,\n isDraft: item.isDraft,\n senderName: getMessageListParticipantLabel(item, folder, t),\n hasObjects: item.hasObjects,\n objectCount: item.objectCount,\n hasAttachments: item.hasAttachments,\n attachmentCount: item.attachmentCount,\n recipientCount: item.recipientCount ?? 0,\n hasActions: item.hasActions,\n actionTaken: item.actionTaken ?? null,\n unread: item.status === 'unread',\n }}\n onClick={() => router.push(`/backend/messages/${item.id}`)}\n />\n )\n },\n },\n ], [folder, listItemComponentKeyByType, messageTypeLabelMap, messageUiRegistry, router, t])\n\n const folderOptions = React.useMemo(() => [\n { id: 'inbox' as const, label: t('messages.folder.inbox', 'Inbox'), icon: Inbox },\n { id: 'sent' as const, label: t('messages.folder.sent', 'Sent'), icon: Send },\n { id: 'drafts' as const, label: t('messages.folder.drafts', 'Drafts'), icon: FilePenLine },\n { id: 'archived' as const, label: t('messages.folder.archived', 'Archived'), icon: Archive },\n { id: 'all' as const, label: t('messages.folder.all', 'All'), icon: Layers },\n ], [t])\n\n const activeFolderOption = folderOptions.find((option) => option.id === folder) ?? folderOptions[0]\n const ActiveFolderIcon = activeFolderOption.icon\n\n React.useEffect(() => {\n if (!folderMenuOpen) return\n const handleClickOutside = (event: MouseEvent) => {\n if (!folderMenuRef.current) return\n const target = event.target\n if (target instanceof Node && !folderMenuRef.current.contains(target)) {\n setFolderMenuOpen(false)\n }\n }\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setFolderMenuOpen(false)\n }\n document.addEventListener('mousedown', handleClickOutside)\n document.addEventListener('keydown', handleEscape)\n return () => {\n document.removeEventListener('mousedown', handleClickOutside)\n document.removeEventListener('keydown', handleEscape)\n }\n }, [folderMenuOpen])\n\n const rows = listQuery.data?.items ?? []\n const total = listQuery.data?.total ?? 0\n const totalIsCapped = listQuery.data?.totalIsCapped === true\n const totalPages = listQuery.data?.totalPages ?? 0\n\n return (\n <div className=\"space-y-4\">\n <DataTable\n title={t('messages.title', 'Messages')}\n titleHeadingLevel={1}\n // UMES extension surface \u2014 opt into widget injection at:\n // data-table:messages:columns / :row-actions / :bulk-actions / :filters / :toolbar / :search-trailing\n // (SPEC-045d \u00A79.3a \u2014 communication_channels hub renders channel badge + delivery status here)\n extensionTableId={extensionPoints.hosts.inboxTable.tableId}\n columns={columns}\n data={rows}\n bulkActions={bulkActions}\n selectionScopeKey={selectionScopeKey}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={t('messages.searchPlaceholder', 'Search messages')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(value) => {\n setFilterValues(value)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n injectionContext={injectionContext}\n isLoading={listQuery.isLoading || listQuery.isFetching}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n totalIsCapped,\n onPageChange: setPage,\n }}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative\" ref={folderMenuRef}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n aria-expanded={folderMenuOpen}\n aria-haspopup=\"menu\"\n onClick={() => setFolderMenuOpen((value) => !value)}\n >\n <ActiveFolderIcon className=\"h-4 w-4\" aria-hidden />\n <span>{t('messages.folder.selector', 'Folder')}:</span>\n <span>{activeFolderOption.label}</span>\n <ChevronDown className=\"h-4 w-4 opacity-70\" aria-hidden />\n </Button>\n {folderMenuOpen ? (\n <div\n className=\"absolute right-0 z-dropdown mt-1 min-w-52 rounded-md border bg-background p-1 shadow\"\n role=\"menu\"\n >\n {folderOptions.map((option) => {\n const Icon = option.icon\n const isActive = option.id === folder\n return (\n <Button\n key={option.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"menuitemradio\"\n aria-checked={isActive}\n className={`w-full justify-start h-auto px-2 py-1.5 text-sm font-normal ${isActive ? 'bg-accent/60' : ''}`}\n onClick={() => {\n setFolder(option.id)\n setPage(1)\n setFolderMenuOpen(false)\n }}\n >\n <Icon className=\"h-4 w-4\" aria-hidden />\n <span>{option.label}</span>\n </Button>\n )\n })}\n </div>\n ) : null}\n </div>\n <Button asChild>\n <Link href=\"/backend/messages/compose\">{t('messages.compose', 'Compose message')}</Link>\n </Button>\n </div>\n }\n onRowClick={(row) => {\n router.push(`/backend/messages/${row.id}`)\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('messages.title', 'Messages')}\n createHref=\"/backend/messages/compose\"\n createLabel={t('messages.compose', 'Compose message')}\n />\n )}\n embedded\n />\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,14 +3,14 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
4
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
5
5
|
import { CheckCircle2, FileText, Paperclip, Zap } from "lucide-react";
|
|
6
|
-
function formatDateTime(value) {
|
|
7
|
-
if (!value) return
|
|
8
|
-
if (Number.isNaN(value.getTime())) return
|
|
6
|
+
function formatDateTime(value, unknownDate) {
|
|
7
|
+
if (!value) return unknownDate;
|
|
8
|
+
if (Number.isNaN(value.getTime())) return unknownDate;
|
|
9
9
|
return value.toLocaleString();
|
|
10
10
|
}
|
|
11
|
-
function formatSentTime(value) {
|
|
12
|
-
if (!value) return
|
|
13
|
-
if (Number.isNaN(value.getTime())) return
|
|
11
|
+
function formatSentTime(value, unknownDate) {
|
|
12
|
+
if (!value) return unknownDate;
|
|
13
|
+
if (Number.isNaN(value.getTime())) return unknownDate;
|
|
14
14
|
const now = /* @__PURE__ */ new Date();
|
|
15
15
|
const isSameDay = now.toDateString() === value.toDateString();
|
|
16
16
|
if (isSameDay) {
|
|
@@ -33,8 +33,10 @@ function DefaultMessageListItem({ message, onClick }) {
|
|
|
33
33
|
const t = useT();
|
|
34
34
|
const senderLabel = message.senderName?.trim() || t("messages.list.noRecipient", "(No recipient)");
|
|
35
35
|
const subject = message.subject.trim() || t("messages.list.noSubject", "(No subject)");
|
|
36
|
-
const
|
|
37
|
-
const
|
|
36
|
+
const unknownDate = t("messages.unknownDate", "Unknown date");
|
|
37
|
+
const notSentYet = "\u2014";
|
|
38
|
+
const absoluteSentAt = message.isDraft ? notSentYet : formatDateTime(message.sentAt, unknownDate);
|
|
39
|
+
const sentAtLabel = message.isDraft ? notSentYet : formatSentTime(message.sentAt, unknownDate);
|
|
38
40
|
const bodyPreview = truncateWords(message.body || "", 16);
|
|
39
41
|
return /* @__PURE__ */ jsx(
|
|
40
42
|
"div",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/messages/components/defaults/DefaultMessageListItem.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport type { MessageListItemProps } from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { CheckCircle2, FileText, Paperclip, Zap } from 'lucide-react'\n\nfunction formatDateTime(value: Date | null): string {\n if (!value) return
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport type { MessageListItemProps } from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { CheckCircle2, FileText, Paperclip, Zap } from 'lucide-react'\n\nfunction formatDateTime(value: Date | null, unknownDate: string): string {\n if (!value) return unknownDate\n if (Number.isNaN(value.getTime())) return unknownDate\n return value.toLocaleString()\n}\n\nfunction formatSentTime(value: Date | null, unknownDate: string): string {\n if (!value) return unknownDate\n if (Number.isNaN(value.getTime())) return unknownDate\n\n const now = new Date()\n const isSameDay = now.toDateString() === value.toDateString()\n if (isSameDay) {\n return value.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })\n }\n\n const isSameYear = now.getFullYear() === value.getFullYear()\n if (isSameYear) {\n return value.toLocaleDateString([], { month: 'short', day: 'numeric' })\n }\n\n return value.toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' })\n}\n\nfunction truncateWords(value: string, maxWords: number): string {\n const normalized = value.trim().replace(/\\s+/g, ' ')\n if (!normalized) return ''\n const words = normalized.split(' ')\n if (words.length <= maxWords) return normalized\n return `${words.slice(0, maxWords).join(' ')}...`\n}\n\nexport function DefaultMessageListItem({ message, onClick }: MessageListItemProps) {\n const t = useT()\n const senderLabel = message.senderName?.trim() || t('messages.list.noRecipient', '(No recipient)')\n const subject = message.subject.trim() || t('messages.list.noSubject', '(No subject)')\n const unknownDate = t('messages.unknownDate', 'Unknown date')\n const notSentYet = '\u2014'\n const absoluteSentAt = message.isDraft ? notSentYet : formatDateTime(message.sentAt, unknownDate)\n const sentAtLabel = message.isDraft ? notSentYet : formatSentTime(message.sentAt, unknownDate)\n const bodyPreview = truncateWords(message.body || '', 16)\n\n return (\n <div\n className={cn(\n 'group min-w-0 w-full cursor-pointer rounded-md px-2 py-2 transition-colors hover:bg-muted/50',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n role=\"button\"\n tabIndex={0}\n aria-label={t('messages.openMessageDetailedA11y', 'Open message {subject} from {sender}, sent {sentAt}', {\n subject,\n sender: senderLabel,\n sentAt: absoluteSentAt,\n })}\n onClick={onClick}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n }}\n >\n <div className=\"flex items-center gap-3\">\n <span className={cn('w-40 flex-shrink-0 truncate text-sm md:w-56', message.unread ? 'font-semibold' : 'font-normal')}>\n {senderLabel}\n </span>\n\n <div className=\"flex min-w-0 flex-1 items-center gap-1.5 text-sm\">\n <span className={cn('truncate text-foreground', message.unread ? 'font-semibold' : 'font-normal')}>\n {subject}\n </span>\n {bodyPreview ? (\n <>\n <span className=\"text-muted-foreground\">-</span>\n <span className=\"truncate text-muted-foreground\">{bodyPreview}</span>\n </>\n ) : null}\n </div>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5 text-muted-foreground\">\n {message.hasObjects ? <FileText className=\"h-3.5 w-3.5\" aria-hidden /> : null}\n {message.hasAttachments ? <Paperclip className=\"h-3.5 w-3.5\" aria-hidden /> : null}\n {message.hasActions && !message.actionTaken ? (\n <Zap className=\"h-3.5 w-3.5 text-amber-600\" aria-hidden />\n ) : null}\n {message.actionTaken ? <CheckCircle2 className=\"h-3.5 w-3.5 text-green-600\" aria-hidden /> : null}\n </div>\n\n <span\n className={cn('flex-shrink-0 text-xs', message.unread ? 'font-semibold text-foreground' : 'font-normal text-muted-foreground')}\n title={absoluteSentAt}\n >\n {sentAtLabel}\n </span>\n </div>\n </div>\n )\n}\n\nexport default DefaultMessageListItem\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -87,7 +87,7 @@ function MessageHeader(props) {
|
|
|
87
87
|
mode: "detail",
|
|
88
88
|
title: showSubject ? props.detail.subject : void 0,
|
|
89
89
|
subtitle: `${t("messages.detail.from", "From")}: ${senderLabel}`,
|
|
90
|
-
statusBadge: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: formatDateTime(props.detail.sentAt) }),
|
|
90
|
+
statusBadge: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: props.detail.isDraft ? "\u2014" : formatDateTime(props.detail.sentAt, t("messages.unknownDate", "Unknown date")) }),
|
|
91
91
|
utilityActions: canReply ? /* @__PURE__ */ jsx(
|
|
92
92
|
IconButton,
|
|
93
93
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/messages/components/message-detail/panels/MessageHeader.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormHeader, type ActionItem } from '@open-mercato/ui/backend/forms'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n Archive,\n ArchiveRestore,\n ChevronDown,\n ChevronUp,\n FilePenLine,\n Forward,\n Mail,\n MailOpen,\n Reply,\n Trash2,\n} from 'lucide-react'\nimport type { MessageDetail } from '../types'\nimport { formatDateTime } from '../utils'\nimport { getMessageParticipantLabel } from '../../messageListLabels'\n\ntype MessageHeaderProps = {\n detail: MessageDetail\n updatingState: boolean\n isArchived: boolean\n showSubject?: boolean\n onReply: () => void\n onForward: () => void\n onEdit: () => void\n onToggleRead: () => void\n onToggleArchive: () => void\n onDelete: () => void\n collapseToggle?: {\n expanded: boolean\n onToggle: () => void\n }\n}\n\nexport function MessageHeader(props: MessageHeaderProps) {\n const t = useT()\n const showSubject = props.showSubject !== false\n const canReply = !props.detail.isDraft && props.detail.typeDefinition.allowReply\n const senderLabel = getMessageParticipantLabel(props.detail)\n const actionItems = React.useMemo(() => {\n const items: ActionItem[] = []\n\n if (props.detail.isDraft && props.detail.canEditDraft) {\n items.push({\n id: 'edit',\n label: t('messages.actions.edit', 'Edit'),\n icon: FilePenLine,\n onSelect: props.onEdit,\n })\n }\n\n if (!props.detail.isDraft && props.detail.typeDefinition.allowForward) {\n items.push({\n id: 'forward',\n label: t('messages.forward', 'Forward'),\n icon: Forward,\n onSelect: props.onForward,\n })\n }\n\n if (!props.detail.isDraft) {\n items.push({\n id: 'toggle-read',\n label: props.detail.isRead\n ? t('messages.actions.markUnread', 'Mark unread')\n : t('messages.actions.markRead', 'Mark read'),\n icon: props.detail.isRead ? Mail : MailOpen,\n onSelect: props.onToggleRead,\n disabled: props.updatingState,\n })\n }\n\n if (!props.detail.isDraft && props.detail.canArchive) {\n items.push({\n id: 'toggle-archive',\n label: props.isArchived\n ? t('messages.actions.unarchive', 'Unarchive')\n : t('messages.actions.archive', 'Archive'),\n icon: props.isArchived ? ArchiveRestore : Archive,\n onSelect: props.onToggleArchive,\n disabled: props.updatingState,\n })\n }\n\n items.push({\n id: 'delete',\n label: t('messages.actions.delete', 'Delete'),\n icon: Trash2,\n onSelect: props.onDelete,\n disabled: props.updatingState,\n })\n\n return items\n }, [props, t])\n\n return (\n <div className=\"flex items-start gap-2\">\n {props.collapseToggle ? (\n <IconButton\n type=\"button\"\n onClick={props.collapseToggle.onToggle}\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-1 text-muted-foreground hover:text-foreground\"\n aria-label={props.collapseToggle.expanded\n ? t('messages.detail.hideDetails', 'Hide details')\n : t('messages.detail.showDetails', 'Show details')}\n >\n {props.collapseToggle.expanded ? (\n <ChevronUp className=\"h-4 w-4\" aria-hidden />\n ) : (\n <ChevronDown className=\"h-4 w-4\" aria-hidden />\n )}\n </IconButton>\n ) : null}\n\n <div className=\"min-w-0 flex-1\">\n <FormHeader\n mode=\"detail\"\n title={showSubject ? props.detail.subject : undefined}\n subtitle={`${t('messages.detail.from', 'From')}: ${senderLabel}`}\n statusBadge={<p className=\"text-xs text-muted-foreground\">{formatDateTime(props.detail.sentAt)}</p>}\n utilityActions={canReply ? (\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"default\"\n aria-label={t('messages.reply', 'Reply')}\n onClick={props.onReply}\n >\n <Reply className=\"h-4 w-4\" aria-hidden />\n </IconButton>\n ) : undefined}\n menuTriggerMode=\"icon\"\n menuAriaLabel={t('ui.actions.actions', 'Actions')}\n menuActions={actionItems}\n />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAqGI,SAaQ,KAbR;AAnGJ,YAAY,WAAW;AACvB,SAAS,YAAY;AACrB,SAAS,kBAAmC;AAC5C,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,sBAAsB;AAC/B,SAAS,kCAAkC;AAmBpC,SAAS,cAAc,OAA2B;AACvD,QAAM,IAAI,KAAK;AACf,QAAM,cAAc,MAAM,gBAAgB;AAC1C,QAAM,WAAW,CAAC,MAAM,OAAO,WAAW,MAAM,OAAO,eAAe;AACtE,QAAM,cAAc,2BAA2B,MAAM,MAAM;AAC3D,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,UAAM,QAAsB,CAAC;AAE7B,QAAI,MAAM,OAAO,WAAW,MAAM,OAAO,cAAc;AACrD,YAAM,KAAK;AAAA,QACT,IAAI;AAAA,QACJ,OAAO,EAAE,yBAAyB,MAAM;AAAA,QACxC,MAAM;AAAA,QACN,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,QAAI,CAAC,MAAM,OAAO,WAAW,MAAM,OAAO,eAAe,cAAc;AACrE,YAAM,KAAK;AAAA,QACT,IAAI;AAAA,QACJ,OAAO,EAAE,oBAAoB,SAAS;AAAA,QACtC,MAAM;AAAA,QACN,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,QAAI,CAAC,MAAM,OAAO,SAAS;AACzB,YAAM,KAAK;AAAA,QACT,IAAI;AAAA,QACJ,OAAO,MAAM,OAAO,SAChB,EAAE,+BAA+B,aAAa,IAC9C,EAAE,6BAA6B,WAAW;AAAA,QAC9C,MAAM,MAAM,OAAO,SAAS,OAAO;AAAA,QACnC,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,QAAI,CAAC,MAAM,OAAO,WAAW,MAAM,OAAO,YAAY;AACpD,YAAM,KAAK;AAAA,QACT,IAAI;AAAA,QACJ,OAAO,MAAM,aACT,EAAE,8BAA8B,WAAW,IAC3C,EAAE,4BAA4B,SAAS;AAAA,QAC3C,MAAM,MAAM,aAAa,iBAAiB;AAAA,QAC1C,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,UAAM,KAAK;AAAA,MACT,IAAI;AAAA,MACJ,OAAO,EAAE,2BAA2B,QAAQ;AAAA,MAC5C,MAAM;AAAA,MACN,UAAU,MAAM;AAAA,MAChB,UAAU,MAAM;AAAA,IAClB,CAAC;AAED,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,CAAC,CAAC;AAEb,SACE,qBAAC,SAAI,WAAU,0BACZ;AAAA,UAAM,iBACL;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,eAAe;AAAA,QAC9B,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,WAAU;AAAA,QACV,cAAY,MAAM,eAAe,WAC7B,EAAE,+BAA+B,cAAc,IAC/C,EAAE,+BAA+B,cAAc;AAAA,QAElD,gBAAM,eAAe,WACpB,oBAAC,aAAU,WAAU,WAAU,eAAW,MAAC,IAE3C,oBAAC,eAAY,WAAU,WAAU,eAAW,MAAC;AAAA;AAAA,IAEjD,IACE;AAAA,IAEJ,oBAAC,SAAI,WAAU,kBACb;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,cAAc,MAAM,OAAO,UAAU;AAAA,QAC5C,UAAU,GAAG,EAAE,wBAAwB,MAAM,CAAC,KAAK,WAAW;AAAA,QAC9D,aAAa,oBAAC,OAAE,WAAU,iCAAiC,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormHeader, type ActionItem } from '@open-mercato/ui/backend/forms'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n Archive,\n ArchiveRestore,\n ChevronDown,\n ChevronUp,\n FilePenLine,\n Forward,\n Mail,\n MailOpen,\n Reply,\n Trash2,\n} from 'lucide-react'\nimport type { MessageDetail } from '../types'\nimport { formatDateTime } from '../utils'\nimport { getMessageParticipantLabel } from '../../messageListLabels'\n\ntype MessageHeaderProps = {\n detail: MessageDetail\n updatingState: boolean\n isArchived: boolean\n showSubject?: boolean\n onReply: () => void\n onForward: () => void\n onEdit: () => void\n onToggleRead: () => void\n onToggleArchive: () => void\n onDelete: () => void\n collapseToggle?: {\n expanded: boolean\n onToggle: () => void\n }\n}\n\nexport function MessageHeader(props: MessageHeaderProps) {\n const t = useT()\n const showSubject = props.showSubject !== false\n const canReply = !props.detail.isDraft && props.detail.typeDefinition.allowReply\n const senderLabel = getMessageParticipantLabel(props.detail)\n const actionItems = React.useMemo(() => {\n const items: ActionItem[] = []\n\n if (props.detail.isDraft && props.detail.canEditDraft) {\n items.push({\n id: 'edit',\n label: t('messages.actions.edit', 'Edit'),\n icon: FilePenLine,\n onSelect: props.onEdit,\n })\n }\n\n if (!props.detail.isDraft && props.detail.typeDefinition.allowForward) {\n items.push({\n id: 'forward',\n label: t('messages.forward', 'Forward'),\n icon: Forward,\n onSelect: props.onForward,\n })\n }\n\n if (!props.detail.isDraft) {\n items.push({\n id: 'toggle-read',\n label: props.detail.isRead\n ? t('messages.actions.markUnread', 'Mark unread')\n : t('messages.actions.markRead', 'Mark read'),\n icon: props.detail.isRead ? Mail : MailOpen,\n onSelect: props.onToggleRead,\n disabled: props.updatingState,\n })\n }\n\n if (!props.detail.isDraft && props.detail.canArchive) {\n items.push({\n id: 'toggle-archive',\n label: props.isArchived\n ? t('messages.actions.unarchive', 'Unarchive')\n : t('messages.actions.archive', 'Archive'),\n icon: props.isArchived ? ArchiveRestore : Archive,\n onSelect: props.onToggleArchive,\n disabled: props.updatingState,\n })\n }\n\n items.push({\n id: 'delete',\n label: t('messages.actions.delete', 'Delete'),\n icon: Trash2,\n onSelect: props.onDelete,\n disabled: props.updatingState,\n })\n\n return items\n }, [props, t])\n\n return (\n <div className=\"flex items-start gap-2\">\n {props.collapseToggle ? (\n <IconButton\n type=\"button\"\n onClick={props.collapseToggle.onToggle}\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-1 text-muted-foreground hover:text-foreground\"\n aria-label={props.collapseToggle.expanded\n ? t('messages.detail.hideDetails', 'Hide details')\n : t('messages.detail.showDetails', 'Show details')}\n >\n {props.collapseToggle.expanded ? (\n <ChevronUp className=\"h-4 w-4\" aria-hidden />\n ) : (\n <ChevronDown className=\"h-4 w-4\" aria-hidden />\n )}\n </IconButton>\n ) : null}\n\n <div className=\"min-w-0 flex-1\">\n <FormHeader\n mode=\"detail\"\n title={showSubject ? props.detail.subject : undefined}\n subtitle={`${t('messages.detail.from', 'From')}: ${senderLabel}`}\n statusBadge={<p className=\"text-xs text-muted-foreground\">{props.detail.isDraft\n ? '\u2014'\n : formatDateTime(props.detail.sentAt, t('messages.unknownDate', 'Unknown date'))}</p>}\n utilityActions={canReply ? (\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"default\"\n aria-label={t('messages.reply', 'Reply')}\n onClick={props.onReply}\n >\n <Reply className=\"h-4 w-4\" aria-hidden />\n </IconButton>\n ) : undefined}\n menuTriggerMode=\"icon\"\n menuAriaLabel={t('ui.actions.actions', 'Actions')}\n menuActions={actionItems}\n />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
4
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
5
|
-
function formatSentTime(value) {
|
|
6
|
-
if (!value || Number.isNaN(value.getTime())) return
|
|
5
|
+
function formatSentTime(value, unknownDate) {
|
|
6
|
+
if (!value || Number.isNaN(value.getTime())) return unknownDate;
|
|
7
7
|
const now = /* @__PURE__ */ new Date();
|
|
8
8
|
if (now.toDateString() === value.toDateString()) {
|
|
9
9
|
return value.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
@@ -18,7 +18,7 @@ function MessageListComponent({ message, onClick }) {
|
|
|
18
18
|
const t = useT();
|
|
19
19
|
const senderLabel = message.senderName || "\u2014";
|
|
20
20
|
const body = normalizeBody(message.body);
|
|
21
|
-
const sentAtLabel = formatSentTime(message.sentAt);
|
|
21
|
+
const sentAtLabel = message.isDraft ? "\u2014" : formatSentTime(message.sentAt, t("messages.unknownDate", "Unknown date"));
|
|
22
22
|
return /* @__PURE__ */ jsx(
|
|
23
23
|
"div",
|
|
24
24
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/messages/components/message-detail/panels/MessageListComponent.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport type { MessageListItemProps } from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nfunction formatSentTime(value: Date | null): string {\n if (!value || Number.isNaN(value.getTime())) return
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport type { MessageListItemProps } from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nfunction formatSentTime(value: Date | null, unknownDate: string): string {\n if (!value || Number.isNaN(value.getTime())) return unknownDate\n\n const now = new Date()\n if (now.toDateString() === value.toDateString()) {\n return value.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })\n }\n\n return value.toLocaleDateString([], { month: 'short', day: 'numeric' })\n}\n\nfunction normalizeBody(value: string | null): string {\n const normalized = (value ?? '').trim().replace(/\\s+/g, ' ')\n return normalized || '\u2014'\n}\n\nexport function MessageListComponent({ message, onClick }: MessageListItemProps) {\n const t = useT()\n const senderLabel = message.senderName || '\u2014'\n const body = normalizeBody(message.body)\n const sentAtLabel = message.isDraft\n ? '\u2014'\n : formatSentTime(message.sentAt, t('messages.unknownDate', 'Unknown date'))\n\n return (\n <div\n className={cn(\n 'group min-w-0 w-full cursor-pointer rounded-md px-2 py-2 transition-colors hover:bg-muted/50',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n )}\n role=\"button\"\n tabIndex={0}\n aria-label={t('messages.openMessageA11y', 'Open message from {sender}', { sender: senderLabel })}\n onClick={onClick}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n }}\n >\n <div className=\"flex items-center gap-3\">\n <span className={cn('w-40 flex-shrink-0 truncate text-sm md:w-56', message.unread ? 'font-semibold' : 'font-normal')}>\n {senderLabel}\n </span>\n\n <span className=\"min-w-0 flex-1 truncate text-sm text-muted-foreground\">{body}</span>\n\n <span\n className={cn('flex-shrink-0 text-xs', message.unread ? 'font-semibold text-foreground' : 'font-normal text-muted-foreground')}\n >\n {sentAtLabel}\n </span>\n </div>\n </div>\n )\n}\n\nexport default MessageListComponent\n"],
|
|
5
|
+
"mappings": ";AA+CM,SACE,KADF;AA5CN,SAAS,YAAY;AACrB,SAAS,UAAU;AAEnB,SAAS,eAAe,OAAoB,aAA6B;AACvE,MAAI,CAAC,SAAS,OAAO,MAAM,MAAM,QAAQ,CAAC,EAAG,QAAO;AAEpD,QAAM,MAAM,oBAAI,KAAK;AACrB,MAAI,IAAI,aAAa,MAAM,MAAM,aAAa,GAAG;AAC/C,WAAO,MAAM,mBAAmB,CAAC,GAAG,EAAE,MAAM,WAAW,QAAQ,UAAU,CAAC;AAAA,EAC5E;AAEA,SAAO,MAAM,mBAAmB,CAAC,GAAG,EAAE,OAAO,SAAS,KAAK,UAAU,CAAC;AACxE;AAEA,SAAS,cAAc,OAA8B;AACnD,QAAM,cAAc,SAAS,IAAI,KAAK,EAAE,QAAQ,QAAQ,GAAG;AAC3D,SAAO,cAAc;AACvB;AAEO,SAAS,qBAAqB,EAAE,SAAS,QAAQ,GAAyB;AAC/E,QAAM,IAAI,KAAK;AACf,QAAM,cAAc,QAAQ,cAAc;AAC1C,QAAM,OAAO,cAAc,QAAQ,IAAI;AACvC,QAAM,cAAc,QAAQ,UACxB,WACA,eAAe,QAAQ,QAAQ,EAAE,wBAAwB,cAAc,CAAC;AAE5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACA,MAAK;AAAA,MACL,UAAU;AAAA,MACV,cAAY,EAAE,4BAA4B,8BAA8B,EAAE,QAAQ,YAAY,CAAC;AAAA,MAC/F;AAAA,MACA,WAAW,CAAC,UAAU;AACpB,YAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;AAC9C,gBAAM,eAAe;AACrB,kBAAQ;AAAA,QACV;AAAA,MACF;AAAA,MAEA,+BAAC,SAAI,WAAU,2BACb;AAAA,4BAAC,UAAK,WAAW,GAAG,+CAA+C,QAAQ,SAAS,kBAAkB,aAAa,GAChH,uBACH;AAAA,QAEA,oBAAC,UAAK,WAAU,yDAAyD,gBAAK;AAAA,QAE9E;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,GAAG,yBAAyB,QAAQ,SAAS,kCAAkC,mCAAmC;AAAA,YAE5H;AAAA;AAAA,QACH;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,+BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -16,7 +16,7 @@ function MessageDetailThreadSection({ detail }) {
|
|
|
16
16
|
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
17
17
|
getMessageParticipantLabel(threadItem),
|
|
18
18
|
" \u2022 ",
|
|
19
|
-
formatDateTime(threadItem.sentAt)
|
|
19
|
+
formatDateTime(threadItem.sentAt, t("messages.unknownDate", "Unknown date"))
|
|
20
20
|
] }),
|
|
21
21
|
/* @__PURE__ */ jsx("div", { className: "mt-2 max-h-[60vh] overflow-y-auto pr-1", children: /* @__PURE__ */ jsx(
|
|
22
22
|
MarkdownContent,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/messages/components/message-detail/panels/thread-panel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent'\nimport { getMessageUiComponentRegistry } from '../../utils/typeUiRegistry'\nimport { getMessageObjectType } from '../../../lib/message-objects-registry'\nimport type { MessageDetail, MessageDetailObject } from '../types'\nimport { formatDateTime } from '../utils'\nimport { getMessageParticipantLabel } from '../../messageListLabels'\n\nexport function MessageDetailThreadSection({ detail }: { detail: MessageDetail }) {\n const t = useT()\n const messageUiRegistry = getMessageUiComponentRegistry()\n\n if ((detail.thread ?? []).length === 0) return null\n\n return (\n <section className=\"space-y-3 border-l pl-4 py-2\">\n <h2 className=\"text-base font-semibold\">{t('messages.detail.thread', 'Thread')}</h2>\n <div className=\"space-y-3\">\n {(detail.thread ?? []).map((threadItem) => (\n <article key={threadItem.id} className=\"rounded border p-3\">\n <p className=\"text-xs text-muted-foreground\">\n {getMessageParticipantLabel(threadItem)} \u2022 {formatDateTime(threadItem.sentAt)}\n </p>\n <div className=\"mt-2 max-h-[60vh] overflow-y-auto pr-1\">\n <MarkdownContent\n body={threadItem.body}\n format={threadItem.bodyFormat ?? 'text'}\n className=\"text-sm whitespace-pre-wrap [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs\"\n />\n </div>\n\n {(threadItem.objects ?? []).length > 0 && (\n <div className=\"mt-3 space-y-2\">\n <p className=\"text-xs text-muted-foreground font-medium\">\n {t('messages.attachedObjects', 'Attached objects')}\n </p>\n {(threadItem.objects ?? []).map((obj: MessageDetailObject) => {\n const componentKey = `${obj.entityModule}:${obj.entityType}`\n const PreviewComponent = messageUiRegistry.objectPreviewComponents[componentKey]\n ?? messageUiRegistry.objectPreviewComponents['messages:default']\n const objectType = getMessageObjectType(obj.entityModule, obj.entityType)\n if (!PreviewComponent) return null\n\n return (\n <div key={obj.id} className=\"scale-95 origin-left\">\n <PreviewComponent\n entityId={obj.entityId}\n entityModule={obj.entityModule}\n entityType={obj.entityType}\n snapshot={obj.snapshot ?? undefined}\n previewData={obj.preview ?? undefined}\n actionRequired={obj.actionRequired}\n actionType={obj.actionType ?? undefined}\n actionLabel={obj.actionLabel ?? undefined}\n icon={objectType?.icon}\n />\n </div>\n )\n })}\n </div>\n )}\n </article>\n ))}\n </div>\n </section>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAkBM,cAIM,YAJN;AAhBN,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAChC,SAAS,qCAAqC;AAC9C,SAAS,4BAA4B;AAErC,SAAS,sBAAsB;AAC/B,SAAS,kCAAkC;AAEpC,SAAS,2BAA2B,EAAE,OAAO,GAA8B;AAChF,QAAM,IAAI,KAAK;AACf,QAAM,oBAAoB,8BAA8B;AAExD,OAAK,OAAO,UAAU,CAAC,GAAG,WAAW,EAAG,QAAO;AAE/C,SACE,qBAAC,aAAQ,WAAU,gCACjB;AAAA,wBAAC,QAAG,WAAU,2BAA2B,YAAE,0BAA0B,QAAQ,GAAE;AAAA,IAC/E,oBAAC,SAAI,WAAU,aACX,kBAAO,UAAU,CAAC,GAAG,IAAI,CAAC,eAC1B,qBAAC,aAA4B,WAAU,sBACrC;AAAA,2BAAC,OAAE,WAAU,iCACV;AAAA,mCAA2B,UAAU;AAAA,QAAE;AAAA,QAAI,eAAe,WAAW,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent'\nimport { getMessageUiComponentRegistry } from '../../utils/typeUiRegistry'\nimport { getMessageObjectType } from '../../../lib/message-objects-registry'\nimport type { MessageDetail, MessageDetailObject } from '../types'\nimport { formatDateTime } from '../utils'\nimport { getMessageParticipantLabel } from '../../messageListLabels'\n\nexport function MessageDetailThreadSection({ detail }: { detail: MessageDetail }) {\n const t = useT()\n const messageUiRegistry = getMessageUiComponentRegistry()\n\n if ((detail.thread ?? []).length === 0) return null\n\n return (\n <section className=\"space-y-3 border-l pl-4 py-2\">\n <h2 className=\"text-base font-semibold\">{t('messages.detail.thread', 'Thread')}</h2>\n <div className=\"space-y-3\">\n {(detail.thread ?? []).map((threadItem) => (\n <article key={threadItem.id} className=\"rounded border p-3\">\n <p className=\"text-xs text-muted-foreground\">\n {getMessageParticipantLabel(threadItem)} \u2022 {formatDateTime(threadItem.sentAt, t('messages.unknownDate', 'Unknown date'))}\n </p>\n <div className=\"mt-2 max-h-[60vh] overflow-y-auto pr-1\">\n <MarkdownContent\n body={threadItem.body}\n format={threadItem.bodyFormat ?? 'text'}\n className=\"text-sm whitespace-pre-wrap [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs\"\n />\n </div>\n\n {(threadItem.objects ?? []).length > 0 && (\n <div className=\"mt-3 space-y-2\">\n <p className=\"text-xs text-muted-foreground font-medium\">\n {t('messages.attachedObjects', 'Attached objects')}\n </p>\n {(threadItem.objects ?? []).map((obj: MessageDetailObject) => {\n const componentKey = `${obj.entityModule}:${obj.entityType}`\n const PreviewComponent = messageUiRegistry.objectPreviewComponents[componentKey]\n ?? messageUiRegistry.objectPreviewComponents['messages:default']\n const objectType = getMessageObjectType(obj.entityModule, obj.entityType)\n if (!PreviewComponent) return null\n\n return (\n <div key={obj.id} className=\"scale-95 origin-left\">\n <PreviewComponent\n entityId={obj.entityId}\n entityModule={obj.entityModule}\n entityType={obj.entityType}\n snapshot={obj.snapshot ?? undefined}\n previewData={obj.preview ?? undefined}\n actionRequired={obj.actionRequired}\n actionType={obj.actionType ?? undefined}\n actionLabel={obj.actionLabel ?? undefined}\n icon={objectType?.icon}\n />\n </div>\n )\n })}\n </div>\n )}\n </article>\n ))}\n </div>\n </section>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAkBM,cAIM,YAJN;AAhBN,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAChC,SAAS,qCAAqC;AAC9C,SAAS,4BAA4B;AAErC,SAAS,sBAAsB;AAC/B,SAAS,kCAAkC;AAEpC,SAAS,2BAA2B,EAAE,OAAO,GAA8B;AAChF,QAAM,IAAI,KAAK;AACf,QAAM,oBAAoB,8BAA8B;AAExD,OAAK,OAAO,UAAU,CAAC,GAAG,WAAW,EAAG,QAAO;AAE/C,SACE,qBAAC,aAAQ,WAAU,gCACjB;AAAA,wBAAC,QAAG,WAAU,2BAA2B,YAAE,0BAA0B,QAAQ,GAAE;AAAA,IAC/E,oBAAC,SAAI,WAAU,aACX,kBAAO,UAAU,CAAC,GAAG,IAAI,CAAC,eAC1B,qBAAC,aAA4B,WAAU,sBACrC;AAAA,2BAAC,OAAE,WAAU,iCACV;AAAA,mCAA2B,UAAU;AAAA,QAAE;AAAA,QAAI,eAAe,WAAW,QAAQ,EAAE,wBAAwB,cAAc,CAAC;AAAA,SACzH;AAAA,MACA,oBAAC,SAAI,WAAU,0CACb;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,WAAW;AAAA,UACjB,QAAQ,WAAW,cAAc;AAAA,UACjC,WAAU;AAAA;AAAA,MACZ,GACF;AAAA,OAEE,WAAW,WAAW,CAAC,GAAG,SAAS,KACnC,qBAAC,SAAI,WAAU,kBACb;AAAA,4BAAC,OAAE,WAAU,6CACV,YAAE,4BAA4B,kBAAkB,GACnD;AAAA,SACE,WAAW,WAAW,CAAC,GAAG,IAAI,CAAC,QAA6B;AAC5D,gBAAM,eAAe,GAAG,IAAI,YAAY,IAAI,IAAI,UAAU;AAC1D,gBAAM,mBAAmB,kBAAkB,wBAAwB,YAAY,KAC1E,kBAAkB,wBAAwB,kBAAkB;AACjE,gBAAM,aAAa,qBAAqB,IAAI,cAAc,IAAI,UAAU;AACxE,cAAI,CAAC,iBAAkB,QAAO;AAE9B,iBACE,oBAAC,SAAiB,WAAU,wBAC1B;AAAA,YAAC;AAAA;AAAA,cACC,UAAU,IAAI;AAAA,cACd,cAAc,IAAI;AAAA,cAClB,YAAY,IAAI;AAAA,cAChB,UAAU,IAAI,YAAY;AAAA,cAC1B,aAAa,IAAI,WAAW;AAAA,cAC5B,gBAAgB,IAAI;AAAA,cACpB,YAAY,IAAI,cAAc;AAAA,cAC9B,aAAa,IAAI,eAAe;AAAA,cAChC,MAAM,YAAY;AAAA;AAAA,UACpB,KAXQ,IAAI,EAYd;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA,SAxCU,WAAW,EA0CzB,CACD,GACH;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -14,10 +14,10 @@ function toErrorMessage(payload) {
|
|
|
14
14
|
}
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
|
-
function formatDateTime(value) {
|
|
18
|
-
if (!value) return
|
|
17
|
+
function formatDateTime(value, unknownDate = "\u2014") {
|
|
18
|
+
if (!value) return unknownDate;
|
|
19
19
|
const date = new Date(value);
|
|
20
|
-
if (Number.isNaN(date.getTime())) return
|
|
20
|
+
if (Number.isNaN(date.getTime())) return unknownDate;
|
|
21
21
|
return date.toLocaleString();
|
|
22
22
|
}
|
|
23
23
|
const SAFE_NAVIGATION_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/messages/components/message-detail/utils.ts"],
|
|
4
|
-
"sourcesContent": ["import type { MessageObjectAction } from '@open-mercato/shared/modules/messages/types'\nimport type { MessageAction } from './types'\n\nexport function toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const nested = toErrorMessage(item)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nexport function formatDateTime(value: string | null | undefined): string {\n if (!value) return
|
|
5
|
-
"mappings": "AAGO,SAAS,eAAe,SAAiC;AAC9D,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,eAAW,QAAQ,SAAS;AAC1B,YAAM,SAAS,eAAe,IAAI;AAClC,UAAI,OAAQ,QAAO;AAAA,IACrB;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,UAAU;AAC/B,UAAM,SAAS;AACf,WACE,eAAe,OAAO,KAAK,KACxB,eAAe,OAAO,OAAO,KAC7B,eAAe,OAAO,MAAM,KAC5B,eAAe,OAAO,OAAO,KAC7B;AAAA,EAEP;AACA,SAAO;AACT;AAEO,SAAS,eAAe,
|
|
4
|
+
"sourcesContent": ["import type { MessageObjectAction } from '@open-mercato/shared/modules/messages/types'\nimport type { MessageAction } from './types'\n\nexport function toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const nested = toErrorMessage(item)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nexport function formatDateTime(value: string | null | undefined, unknownDate = '\u2014'): string {\n if (!value) return unknownDate\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return unknownDate\n return date.toLocaleString()\n}\n\nconst SAFE_NAVIGATION_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:'])\n\nexport function isSafeNavigationHref(value: string): boolean {\n const trimmed = value.trim()\n if (!trimmed) return false\n if (trimmed.startsWith('//')) return false\n if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {\n return true\n }\n const base = typeof window !== 'undefined' ? window.location.href : 'http://localhost'\n try {\n const parsed = new URL(trimmed, base)\n return SAFE_NAVIGATION_SCHEMES.has(parsed.protocol)\n } catch {\n return false\n }\n}\n\nexport function parseObjectActionId(value: string): { objectId: string; actionId: string } | null {\n if (!value.startsWith('object:')) return null\n const [, objectId, ...rest] = value.split(':')\n if (!objectId || rest.length === 0) return null\n return { objectId, actionId: rest.join(':') }\n}\n\nfunction resolveObjectActionVariant(\n variant: MessageAction['variant'],\n): MessageObjectAction['variant'] | undefined {\n if (variant === 'default' || variant === 'secondary' || variant === 'destructive' || variant === 'outline') {\n return variant\n }\n return undefined\n}\n\nexport function toObjectAction(actionId: string, action: MessageAction): MessageObjectAction {\n return {\n id: actionId,\n labelKey: action.labelKey ?? action.label ?? actionId,\n variant: resolveObjectActionVariant(action.variant),\n icon: action.icon,\n commandId: action.commandId,\n href: action.href,\n isTerminal: action.isTerminal,\n confirmRequired: action.confirmRequired,\n confirmMessage: action.confirmMessage,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAGO,SAAS,eAAe,SAAiC;AAC9D,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,eAAW,QAAQ,SAAS;AAC1B,YAAM,SAAS,eAAe,IAAI;AAClC,UAAI,OAAQ,QAAO;AAAA,IACrB;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,UAAU;AAC/B,UAAM,SAAS;AACf,WACE,eAAe,OAAO,KAAK,KACxB,eAAe,OAAO,OAAO,KAC7B,eAAe,OAAO,MAAM,KAC5B,eAAe,OAAO,OAAO,KAC7B;AAAA,EAEP;AACA,SAAO;AACT;AAEO,SAAS,eAAe,OAAkC,cAAc,UAAa;AAC1F,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,KAAK,eAAe;AAC7B;AAEA,MAAM,0BAA0B,oBAAI,IAAI,CAAC,SAAS,UAAU,WAAW,MAAM,CAAC;AAEvE,SAAS,qBAAqB,OAAwB;AAC3D,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,QAAQ,WAAW,IAAI,EAAG,QAAO;AACrC,MAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,IAAI,KAAK,QAAQ,WAAW,KAAK,GAAG;AACpF,WAAO;AAAA,EACT;AACA,QAAM,OAAO,OAAO,WAAW,cAAc,OAAO,SAAS,OAAO;AACpE,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,SAAS,IAAI;AACpC,WAAO,wBAAwB,IAAI,OAAO,QAAQ;AAAA,EACpD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,oBAAoB,OAA8D;AAChG,MAAI,CAAC,MAAM,WAAW,SAAS,EAAG,QAAO;AACzC,QAAM,CAAC,EAAE,UAAU,GAAG,IAAI,IAAI,MAAM,MAAM,GAAG;AAC7C,MAAI,CAAC,YAAY,KAAK,WAAW,EAAG,QAAO;AAC3C,SAAO,EAAE,UAAU,UAAU,KAAK,KAAK,GAAG,EAAE;AAC9C;AAEA,SAAS,2BACP,SAC4C;AAC5C,MAAI,YAAY,aAAa,YAAY,eAAe,YAAY,iBAAiB,YAAY,WAAW;AAC1G,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,eAAe,UAAkB,QAA4C;AAC3F,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,UAAU,OAAO,YAAY,OAAO,SAAS;AAAA,IAC7C,SAAS,2BAA2B,OAAO,OAAO;AAAA,IAClD,MAAM,OAAO;AAAA,IACb,WAAW,OAAO;AAAA,IAClB,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,IACnB,iBAAiB,OAAO;AAAA,IACxB,gBAAgB,OAAO;AAAA,EACzB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -131,9 +131,10 @@ const composeMessageBaseSchema = z.object({
|
|
|
131
131
|
* (#6095). Channel ingest sets it to the provider's timestamp so an imported
|
|
132
132
|
* mailbox keeps its real chronology instead of collapsing onto the import
|
|
133
133
|
* minute. Server-only: the HTTP route strips any client-sent value, so a
|
|
134
|
-
* browser caller cannot backdate a message.
|
|
134
|
+
* browser caller cannot backdate a message. Explicit null preserves unknown
|
|
135
|
+
* external occurrence time; omission uses the send time. Ignored for drafts.
|
|
135
136
|
*/
|
|
136
|
-
sentAt: z.coerce.date().optional()
|
|
137
|
+
sentAt: z.coerce.date().nullable().optional()
|
|
137
138
|
});
|
|
138
139
|
function refineComposeMessage(value, ctx) {
|
|
139
140
|
const isDraft = value.isDraft ?? false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/data/validators.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { parseBooleanFlag } from '@open-mercato/shared/lib/boolean'\nimport { sanitizeRichTextHref } from '@open-mercato/shared/lib/html/sanitizeRichText'\nimport { channelTypeRequiresExternalEmail } from '../lib/channel-sender-identity'\n\nfunction collectDuplicateRecipientIds(\n recipients: Array<{ userId: string }>,\n): string[] {\n const seen = new Set<string>()\n const duplicates = new Set<string>()\n for (const recipient of recipients) {\n if (seen.has(recipient.userId)) {\n duplicates.add(recipient.userId)\n continue\n }\n seen.add(recipient.userId)\n }\n return Array.from(duplicates)\n}\n\nfunction validateDefaultWithObjectsPayload(\n value: {\n type?: string\n objects?: Array<{ actionRequired?: boolean; actionType?: string; actionLabel?: string }>\n },\n ctx: z.RefinementCtx,\n): void {\n if (value.type !== 'messages.defaultWithObjects') return\n\n if (!Array.isArray(value.objects) || value.objects.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects'],\n message: 'at least one object is required for messages.defaultWithObjects',\n })\n return\n }\n\n value.objects.forEach((object, index) => {\n if (object.actionRequired === true) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects', index, 'actionRequired'],\n message: 'actionRequired must be false for messages.defaultWithObjects',\n })\n }\n if (typeof object.actionType === 'string' && object.actionType.trim().length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects', index, 'actionType'],\n message: 'actionType is not allowed for messages.defaultWithObjects',\n })\n }\n if (typeof object.actionLabel === 'string' && object.actionLabel.trim().length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects', index, 'actionLabel'],\n message: 'actionLabel is not allowed for messages.defaultWithObjects',\n })\n }\n })\n}\n\nexport const messageRecipientSchema = z.object({\n userId: z.string().uuid(),\n type: z.enum(['to', 'cc', 'bcc']).optional().default('to'),\n})\n\nexport const messageObjectSchema = z.object({\n entityModule: z.string().min(1),\n entityType: z.string().min(1),\n entityId: z.string().uuid(),\n actionRequired: z.boolean().optional().default(false),\n actionType: z.string().optional(),\n actionLabel: z.string().optional(),\n})\n\nexport const messageActionSchema = z.object({\n id: z.string().min(1),\n label: z.string().min(1),\n labelKey: z.string().optional(),\n variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost']).optional(),\n icon: z.string().optional(),\n commandId: z.string().optional(),\n href: z\n .string()\n .optional()\n .refine(\n (value) => value == null || sanitizeRichTextHref(value) != null,\n { message: '[internal] message action href must be http(s), mailto, tel, or a relative path' },\n ),\n isTerminal: z.boolean().optional(),\n confirmRequired: z.boolean().optional(),\n confirmMessage: z.string().optional(),\n})\n\nexport const messageActionDataSchema = z.object({\n actions: z.array(messageActionSchema),\n primaryActionId: z.string().optional(),\n expiresAt: z.string().datetime().optional(),\n})\n\nconst composeMessageBaseSchema = z.object({\n type: z.string().optional().default('default'),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().min(1).optional(),\n sourceEntityId: z.string().uuid().optional(),\n externalEmail: z.string().email().optional(),\n externalName: z.string().min(1).max(255).optional(),\n /**\n * Channel type the message originates from, when the caller knows it (#4975).\n * Non-email channels (Discord, Slack, SMS\u2026) have senders with no address, so\n * `externalEmail` is not required for them. Resolved server-side \u2014 the HTTP\n * route strips any client-sent value and derives it from the referenced\n * conversation or parent message, so a caller cannot waive the requirement by\n * asserting its own channel type.\n */\n sourceChannelType: z.string().min(1).max(64).optional(),\n /**\n * Set by channel ingest for a message that arrived FROM an external channel\n * (#6093). On such a message `recipients` are not external addressees \u2014 a\n * public message has none \u2014 but the internal users the hub routes it to\n * (the conversation's assignee), so the \"no recipients on a public message\"\n * rule does not apply. Server-only like `sourceChannelType`: the HTTP route\n * strips any client-sent value, so a caller cannot waive the rule by\n * asserting its message came in from a channel.\n */\n inboundFromChannel: z.boolean().optional(),\n recipients: z.array(messageRecipientSchema).max(100).optional().default([]),\n subject: z.string().max(500).optional().default(''),\n body: z.string().max(50000).optional().default(''),\n bodyFormat: z.enum(['text', 'markdown']).optional().default('text'),\n priority: z.enum(['low', 'normal', 'high', 'urgent']).optional().default('normal'),\n objects: z.array(messageObjectSchema).optional(),\n attachmentIds: z.array(z.string().uuid()).optional(),\n attachmentRecordId: z.string().min(1).max(255).optional(),\n actionData: messageActionDataSchema.optional(),\n sendViaEmail: z.boolean().optional().default(false),\n /**\n * Employee-owned communication channel to send this message through instead\n * of the platform sender (#6258). The channel is never trusted from the body:\n * the route hands it to the `communication_channels` send-as-user facade,\n * which resolves it inside the caller's scope and refuses a channel the\n * caller does not own. Absent means the platform sender.\n */\n senderChannelId: z.string().uuid().optional(),\n parentMessageId: z.string().uuid().optional(),\n isDraft: z.boolean().optional().default(false),\n /**\n * When the message was actually sent, if the caller knows better than \"now\"\n * (#6095). Channel ingest sets it to the provider's timestamp so an imported\n * mailbox keeps its real chronology instead of collapsing onto the import\n * minute. Server-only: the HTTP route strips any client-sent value, so a\n * browser caller cannot backdate a message. Ignored for drafts.\n */\n sentAt: z.coerce.date().optional(),\n})\n\ntype ComposeMessageRefinementValue = Omit<\n z.infer<typeof composeMessageBaseSchema>,\n 'sourceChannelType' | 'sentAt'\n> & { sourceChannelType?: string; sentAt?: Date }\n\nfunction refineComposeMessage(value: ComposeMessageRefinementValue, ctx: z.RefinementCtx): void {\n const isDraft = value.isDraft ?? false\n const visibility = value.visibility ?? 'internal'\n const recipientCount = value.recipients.length\n const hasExternalEmail = Boolean(value.externalEmail?.trim())\n const hasSubject = value.subject.trim().length > 0\n const hasBody = value.body.trim().length > 0\n\n if (!isDraft) {\n if (visibility === 'public') {\n // #4975: an external correspondent is only guaranteed to have an address\n // on an email-typed channel. `channelTypeRequiresExternalEmail` fails\n // closed, so an unknown or absent channel type keeps the original rule.\n if (!hasExternalEmail && channelTypeRequiresExternalEmail(value.sourceChannelType)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['externalEmail'],\n message: 'externalEmail is required when visibility is public',\n })\n }\n // #6093: a channel-ingested message is addressed to the assignee of its\n // conversation; that recipient is internal routing, not a second\n // external addressee, so only a user-composed public message is bound\n // by this rule.\n if (recipientCount > 0 && !value.inboundFromChannel) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipients must be empty when visibility is public',\n })\n }\n } else if (recipientCount === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'at least one recipient is required when visibility is internal',\n })\n }\n\n if (!hasSubject) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['subject'],\n message: 'subject is required',\n })\n }\n if (!hasBody) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['body'],\n message: 'body is required',\n })\n }\n }\n\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n\n validateDefaultWithObjectsPayload(value, ctx)\n}\n\n/**\n * Full compose contract, including the server-resolved `sourceChannelType`,\n * the ingest-only `inboundFromChannel`, and the server-supplied `sentAt`. Used\n * by the `messages.messages.compose` command and by the HTTP route AFTER it\n * has resolved the channel type itself.\n */\nexport const composeMessageSchema = composeMessageBaseSchema.superRefine(refineComposeMessage)\n\n/**\n * Client-facing compose contract \u2014 the same rules minus the server-only fields:\n * `sourceChannelType` is never accepted from a request body (#4975), neither is\n * `inboundFromChannel` (#6093) nor `sentAt` (#6095). Published in OpenAPI so the\n * documented request shape matches what `POST /api/messages` actually reads:\n * the route discards any client-sent value for these, resolves the real\n * channel type from the referenced conversation or parent message, and only\n * then validates against {@link composeMessageSchema}.\n */\nexport const composeMessageRequestSchema = composeMessageBaseSchema\n .omit({ sourceChannelType: true, inboundFromChannel: true, sentAt: true })\n .superRefine(refineComposeMessage)\n\nexport const updateDraftSchema = z.object({\n type: z.string().optional(),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().min(1).optional(),\n sourceEntityId: z.string().uuid().optional(),\n externalEmail: z.string().email().optional(),\n externalName: z.string().min(1).max(255).optional(),\n recipients: z.array(messageRecipientSchema).optional(),\n subject: z.string().max(500).optional(),\n body: z.string().max(50000).optional(),\n bodyFormat: z.enum(['text', 'markdown']).optional(),\n priority: z.enum(['low', 'normal', 'high', 'urgent']).optional(),\n objects: z.array(messageObjectSchema).optional(),\n attachmentIds: z.array(z.string().uuid()).optional(),\n actionData: messageActionDataSchema.optional(),\n sendViaEmail: z.boolean().optional(),\n isDraft: z.literal(false).optional(),\n /** Accepted only so the route can refuse it explicitly; zod would otherwise strip it and the send would fall back to the platform sender unnoticed. */\n senderChannelId: z.string().uuid().optional(),\n}).superRefine((value, ctx) => {\n if (value.recipients) {\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n }\n\n validateDefaultWithObjectsPayload(value, ctx)\n})\n\nexport const listMessagesSchema = z.object({\n folder: z.enum(['inbox', 'sent', 'drafts', 'archived', 'all']).optional().default('inbox'),\n status: z.enum(['unread', 'read', 'archived']).optional(),\n type: z.string().optional(),\n visibility: z.enum(['public', 'internal']).optional(),\n sourceEntityType: z.string().optional(),\n sourceEntityId: z.string().uuid().optional(),\n externalEmail: z.string().email().optional(),\n hasObjects: z.string().transform(parseBooleanFlag).optional(),\n hasAttachments: z.string().transform(parseBooleanFlag).optional(),\n hasActions: z.string().transform(parseBooleanFlag).optional(),\n senderId: z.string().uuid().optional(),\n search: z.string().max(200).optional(),\n since: z.string().datetime().optional(),\n page: z.coerce.number().int().min(1).optional().default(1),\n pageSize: z.coerce.number().int().min(1).max(100).optional().default(20),\n})\n\nexport const forwardMessageSchema = z.object({\n recipients: z.array(messageRecipientSchema).min(1).max(100),\n body: z.string().max(50000).optional(),\n additionalBody: z.string().max(10000).optional(),\n includeAttachments: z.boolean().optional().default(true),\n sendViaEmail: z.boolean().optional().default(false),\n}).superRefine((value, ctx) => {\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n})\n\nexport const replyMessageSchema = z.object({\n body: z.string().min(1).max(50000),\n bodyFormat: z.enum(['text', 'markdown']).optional().default('text'),\n recipients: z.array(messageRecipientSchema).max(100).optional(),\n attachmentIds: z.array(z.string().uuid()).optional(),\n attachmentRecordId: z.string().min(1).max(255).optional(),\n replyAll: z.boolean().optional().default(false),\n sendViaEmail: z.boolean().optional().default(false),\n}).superRefine((value, ctx) => {\n if (!value.recipients) return\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n})\n\nexport const executeActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const confirmMessageSchema = z.object({\n messageId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n confirmed: z.boolean().optional().default(true),\n})\n\nexport const messageObjectTypesQuerySchema = z.object({\n messageType: z.string().min(1),\n})\n\nexport const attachmentIdsPayloadSchema = z.object({\n attachmentIds: z.array(z.string().uuid()).min(1).max(100),\n})\n\nexport const unlinkAttachmentPayloadSchema = z.object({\n attachmentId: z.string().uuid().optional(),\n attachmentIds: z.array(z.string().uuid()).min(1).max(100).optional(),\n}).refine(\n (value) => Boolean(value.attachmentId || value.attachmentIds?.length),\n { message: 'attachmentId or attachmentIds is required' }\n)\n\nexport type ComposeMessageInput = z.infer<typeof composeMessageSchema>\nexport type UpdateDraftInput = z.infer<typeof updateDraftSchema>\nexport type ListMessagesInput = z.infer<typeof listMessagesSchema>\nexport type ForwardMessageInput = z.infer<typeof forwardMessageSchema>\nexport type ReplyMessageInput = z.infer<typeof replyMessageSchema>\nexport type ExecuteActionInput = z.infer<typeof executeActionSchema>\nexport type ConfirmMessageInput = z.infer<typeof confirmMessageSchema>\nexport type MessageObjectTypesQueryInput = z.infer<typeof messageObjectTypesQuerySchema>\nexport type AttachmentIdsPayload = z.infer<typeof attachmentIdsPayloadSchema>\nexport type UnlinkAttachmentPayload = z.infer<typeof unlinkAttachmentPayloadSchema>\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { parseBooleanFlag } from '@open-mercato/shared/lib/boolean'\nimport { sanitizeRichTextHref } from '@open-mercato/shared/lib/html/sanitizeRichText'\nimport { channelTypeRequiresExternalEmail } from '../lib/channel-sender-identity'\n\nfunction collectDuplicateRecipientIds(\n recipients: Array<{ userId: string }>,\n): string[] {\n const seen = new Set<string>()\n const duplicates = new Set<string>()\n for (const recipient of recipients) {\n if (seen.has(recipient.userId)) {\n duplicates.add(recipient.userId)\n continue\n }\n seen.add(recipient.userId)\n }\n return Array.from(duplicates)\n}\n\nfunction validateDefaultWithObjectsPayload(\n value: {\n type?: string\n objects?: Array<{ actionRequired?: boolean; actionType?: string; actionLabel?: string }>\n },\n ctx: z.RefinementCtx,\n): void {\n if (value.type !== 'messages.defaultWithObjects') return\n\n if (!Array.isArray(value.objects) || value.objects.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects'],\n message: 'at least one object is required for messages.defaultWithObjects',\n })\n return\n }\n\n value.objects.forEach((object, index) => {\n if (object.actionRequired === true) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects', index, 'actionRequired'],\n message: 'actionRequired must be false for messages.defaultWithObjects',\n })\n }\n if (typeof object.actionType === 'string' && object.actionType.trim().length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects', index, 'actionType'],\n message: 'actionType is not allowed for messages.defaultWithObjects',\n })\n }\n if (typeof object.actionLabel === 'string' && object.actionLabel.trim().length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['objects', index, 'actionLabel'],\n message: 'actionLabel is not allowed for messages.defaultWithObjects',\n })\n }\n })\n}\n\nexport const messageRecipientSchema = z.object({\n userId: z.string().uuid(),\n type: z.enum(['to', 'cc', 'bcc']).optional().default('to'),\n})\n\nexport const messageObjectSchema = z.object({\n entityModule: z.string().min(1),\n entityType: z.string().min(1),\n entityId: z.string().uuid(),\n actionRequired: z.boolean().optional().default(false),\n actionType: z.string().optional(),\n actionLabel: z.string().optional(),\n})\n\nexport const messageActionSchema = z.object({\n id: z.string().min(1),\n label: z.string().min(1),\n labelKey: z.string().optional(),\n variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost']).optional(),\n icon: z.string().optional(),\n commandId: z.string().optional(),\n href: z\n .string()\n .optional()\n .refine(\n (value) => value == null || sanitizeRichTextHref(value) != null,\n { message: '[internal] message action href must be http(s), mailto, tel, or a relative path' },\n ),\n isTerminal: z.boolean().optional(),\n confirmRequired: z.boolean().optional(),\n confirmMessage: z.string().optional(),\n})\n\nexport const messageActionDataSchema = z.object({\n actions: z.array(messageActionSchema),\n primaryActionId: z.string().optional(),\n expiresAt: z.string().datetime().optional(),\n})\n\nconst composeMessageBaseSchema = z.object({\n type: z.string().optional().default('default'),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().min(1).optional(),\n sourceEntityId: z.string().uuid().optional(),\n externalEmail: z.string().email().optional(),\n externalName: z.string().min(1).max(255).optional(),\n /**\n * Channel type the message originates from, when the caller knows it (#4975).\n * Non-email channels (Discord, Slack, SMS\u2026) have senders with no address, so\n * `externalEmail` is not required for them. Resolved server-side \u2014 the HTTP\n * route strips any client-sent value and derives it from the referenced\n * conversation or parent message, so a caller cannot waive the requirement by\n * asserting its own channel type.\n */\n sourceChannelType: z.string().min(1).max(64).optional(),\n /**\n * Set by channel ingest for a message that arrived FROM an external channel\n * (#6093). On such a message `recipients` are not external addressees \u2014 a\n * public message has none \u2014 but the internal users the hub routes it to\n * (the conversation's assignee), so the \"no recipients on a public message\"\n * rule does not apply. Server-only like `sourceChannelType`: the HTTP route\n * strips any client-sent value, so a caller cannot waive the rule by\n * asserting its message came in from a channel.\n */\n inboundFromChannel: z.boolean().optional(),\n recipients: z.array(messageRecipientSchema).max(100).optional().default([]),\n subject: z.string().max(500).optional().default(''),\n body: z.string().max(50000).optional().default(''),\n bodyFormat: z.enum(['text', 'markdown']).optional().default('text'),\n priority: z.enum(['low', 'normal', 'high', 'urgent']).optional().default('normal'),\n objects: z.array(messageObjectSchema).optional(),\n attachmentIds: z.array(z.string().uuid()).optional(),\n attachmentRecordId: z.string().min(1).max(255).optional(),\n actionData: messageActionDataSchema.optional(),\n sendViaEmail: z.boolean().optional().default(false),\n /**\n * Employee-owned communication channel to send this message through instead\n * of the platform sender (#6258). The channel is never trusted from the body:\n * the route hands it to the `communication_channels` send-as-user facade,\n * which resolves it inside the caller's scope and refuses a channel the\n * caller does not own. Absent means the platform sender.\n */\n senderChannelId: z.string().uuid().optional(),\n parentMessageId: z.string().uuid().optional(),\n isDraft: z.boolean().optional().default(false),\n /**\n * When the message was actually sent, if the caller knows better than \"now\"\n * (#6095). Channel ingest sets it to the provider's timestamp so an imported\n * mailbox keeps its real chronology instead of collapsing onto the import\n * minute. Server-only: the HTTP route strips any client-sent value, so a\n * browser caller cannot backdate a message. Explicit null preserves unknown\n * external occurrence time; omission uses the send time. Ignored for drafts.\n */\n sentAt: z.coerce.date().nullable().optional(),\n})\n\ntype ComposeMessageRefinementValue = Omit<\n z.infer<typeof composeMessageBaseSchema>,\n 'sourceChannelType' | 'sentAt'\n> & { sourceChannelType?: string; sentAt?: Date | null }\n\nfunction refineComposeMessage(value: ComposeMessageRefinementValue, ctx: z.RefinementCtx): void {\n const isDraft = value.isDraft ?? false\n const visibility = value.visibility ?? 'internal'\n const recipientCount = value.recipients.length\n const hasExternalEmail = Boolean(value.externalEmail?.trim())\n const hasSubject = value.subject.trim().length > 0\n const hasBody = value.body.trim().length > 0\n\n if (!isDraft) {\n if (visibility === 'public') {\n // #4975: an external correspondent is only guaranteed to have an address\n // on an email-typed channel. `channelTypeRequiresExternalEmail` fails\n // closed, so an unknown or absent channel type keeps the original rule.\n if (!hasExternalEmail && channelTypeRequiresExternalEmail(value.sourceChannelType)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['externalEmail'],\n message: 'externalEmail is required when visibility is public',\n })\n }\n // #6093: a channel-ingested message is addressed to the assignee of its\n // conversation; that recipient is internal routing, not a second\n // external addressee, so only a user-composed public message is bound\n // by this rule.\n if (recipientCount > 0 && !value.inboundFromChannel) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipients must be empty when visibility is public',\n })\n }\n } else if (recipientCount === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'at least one recipient is required when visibility is internal',\n })\n }\n\n if (!hasSubject) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['subject'],\n message: 'subject is required',\n })\n }\n if (!hasBody) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['body'],\n message: 'body is required',\n })\n }\n }\n\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n\n validateDefaultWithObjectsPayload(value, ctx)\n}\n\n/**\n * Full compose contract, including the server-resolved `sourceChannelType`,\n * the ingest-only `inboundFromChannel`, and the server-supplied `sentAt`. Used\n * by the `messages.messages.compose` command and by the HTTP route AFTER it\n * has resolved the channel type itself.\n */\nexport const composeMessageSchema = composeMessageBaseSchema.superRefine(refineComposeMessage)\n\n/**\n * Client-facing compose contract \u2014 the same rules minus the server-only fields:\n * `sourceChannelType` is never accepted from a request body (#4975), neither is\n * `inboundFromChannel` (#6093) nor `sentAt` (#6095). Published in OpenAPI so the\n * documented request shape matches what `POST /api/messages` actually reads:\n * the route discards any client-sent value for these, resolves the real\n * channel type from the referenced conversation or parent message, and only\n * then validates against {@link composeMessageSchema}.\n */\nexport const composeMessageRequestSchema = composeMessageBaseSchema\n .omit({ sourceChannelType: true, inboundFromChannel: true, sentAt: true })\n .superRefine(refineComposeMessage)\n\nexport const updateDraftSchema = z.object({\n type: z.string().optional(),\n visibility: z.enum(['public', 'internal']).nullable().optional(),\n sourceEntityType: z.string().min(1).optional(),\n sourceEntityId: z.string().uuid().optional(),\n externalEmail: z.string().email().optional(),\n externalName: z.string().min(1).max(255).optional(),\n recipients: z.array(messageRecipientSchema).optional(),\n subject: z.string().max(500).optional(),\n body: z.string().max(50000).optional(),\n bodyFormat: z.enum(['text', 'markdown']).optional(),\n priority: z.enum(['low', 'normal', 'high', 'urgent']).optional(),\n objects: z.array(messageObjectSchema).optional(),\n attachmentIds: z.array(z.string().uuid()).optional(),\n actionData: messageActionDataSchema.optional(),\n sendViaEmail: z.boolean().optional(),\n isDraft: z.literal(false).optional(),\n /** Accepted only so the route can refuse it explicitly; zod would otherwise strip it and the send would fall back to the platform sender unnoticed. */\n senderChannelId: z.string().uuid().optional(),\n}).superRefine((value, ctx) => {\n if (value.recipients) {\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n }\n\n validateDefaultWithObjectsPayload(value, ctx)\n})\n\nexport const listMessagesSchema = z.object({\n folder: z.enum(['inbox', 'sent', 'drafts', 'archived', 'all']).optional().default('inbox'),\n status: z.enum(['unread', 'read', 'archived']).optional(),\n type: z.string().optional(),\n visibility: z.enum(['public', 'internal']).optional(),\n sourceEntityType: z.string().optional(),\n sourceEntityId: z.string().uuid().optional(),\n externalEmail: z.string().email().optional(),\n hasObjects: z.string().transform(parseBooleanFlag).optional(),\n hasAttachments: z.string().transform(parseBooleanFlag).optional(),\n hasActions: z.string().transform(parseBooleanFlag).optional(),\n senderId: z.string().uuid().optional(),\n search: z.string().max(200).optional(),\n since: z.string().datetime().optional(),\n page: z.coerce.number().int().min(1).optional().default(1),\n pageSize: z.coerce.number().int().min(1).max(100).optional().default(20),\n})\n\nexport const forwardMessageSchema = z.object({\n recipients: z.array(messageRecipientSchema).min(1).max(100),\n body: z.string().max(50000).optional(),\n additionalBody: z.string().max(10000).optional(),\n includeAttachments: z.boolean().optional().default(true),\n sendViaEmail: z.boolean().optional().default(false),\n}).superRefine((value, ctx) => {\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n})\n\nexport const replyMessageSchema = z.object({\n body: z.string().min(1).max(50000),\n bodyFormat: z.enum(['text', 'markdown']).optional().default('text'),\n recipients: z.array(messageRecipientSchema).max(100).optional(),\n attachmentIds: z.array(z.string().uuid()).optional(),\n attachmentRecordId: z.string().min(1).max(255).optional(),\n replyAll: z.boolean().optional().default(false),\n sendViaEmail: z.boolean().optional().default(false),\n}).superRefine((value, ctx) => {\n if (!value.recipients) return\n const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)\n if (duplicateRecipientIds.length > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['recipients'],\n message: 'recipient user ids must be unique',\n })\n }\n})\n\nexport const executeActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const confirmMessageSchema = z.object({\n messageId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n confirmed: z.boolean().optional().default(true),\n})\n\nexport const messageObjectTypesQuerySchema = z.object({\n messageType: z.string().min(1),\n})\n\nexport const attachmentIdsPayloadSchema = z.object({\n attachmentIds: z.array(z.string().uuid()).min(1).max(100),\n})\n\nexport const unlinkAttachmentPayloadSchema = z.object({\n attachmentId: z.string().uuid().optional(),\n attachmentIds: z.array(z.string().uuid()).min(1).max(100).optional(),\n}).refine(\n (value) => Boolean(value.attachmentId || value.attachmentIds?.length),\n { message: 'attachmentId or attachmentIds is required' }\n)\n\nexport type ComposeMessageInput = z.infer<typeof composeMessageSchema>\nexport type UpdateDraftInput = z.infer<typeof updateDraftSchema>\nexport type ListMessagesInput = z.infer<typeof listMessagesSchema>\nexport type ForwardMessageInput = z.infer<typeof forwardMessageSchema>\nexport type ReplyMessageInput = z.infer<typeof replyMessageSchema>\nexport type ExecuteActionInput = z.infer<typeof executeActionSchema>\nexport type ConfirmMessageInput = z.infer<typeof confirmMessageSchema>\nexport type MessageObjectTypesQueryInput = z.infer<typeof messageObjectTypesQuerySchema>\nexport type AttachmentIdsPayload = z.infer<typeof attachmentIdsPayloadSchema>\nexport type UnlinkAttachmentPayload = z.infer<typeof unlinkAttachmentPayloadSchema>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -31,10 +31,10 @@ function toErrorMessage(payload) {
|
|
|
31
31
|
}
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
-
function formatDateTime(value) {
|
|
35
|
-
if (!value) return
|
|
34
|
+
function formatDateTime(value, unknownDate) {
|
|
35
|
+
if (!value) return unknownDate;
|
|
36
36
|
const date = new Date(value);
|
|
37
|
-
if (Number.isNaN(date.getTime())) return
|
|
37
|
+
if (Number.isNaN(date.getTime())) return unknownDate;
|
|
38
38
|
return date.toLocaleString();
|
|
39
39
|
}
|
|
40
40
|
function toObjectActions(objectActions) {
|
|
@@ -172,8 +172,9 @@ function MessageTokenPage({ params }) {
|
|
|
172
172
|
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: data.subject }),
|
|
173
173
|
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
174
174
|
t("messages.token.sentAt", "Sent"),
|
|
175
|
-
":
|
|
176
|
-
|
|
175
|
+
":",
|
|
176
|
+
" ",
|
|
177
|
+
formatDateTime(data.sentAt, t("messages.unknownDate", "Unknown date"))
|
|
177
178
|
] })
|
|
178
179
|
] }),
|
|
179
180
|
/* @__PURE__ */ jsx("section", { className: "rounded-xl border bg-card p-4", children: ContentComponent ? /* @__PURE__ */ jsx(ContentComponent, { ...contentProps }) : /* @__PURE__ */ jsx(
|