@open-mercato/ui 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435

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.
Files changed (93) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/ai/parts/MutationResultCard.js +1 -14
  3. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  4. package/dist/backend/CrudForm.js +52 -96
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/DataTable.js +14 -40
  7. package/dist/backend/DataTable.js.map +2 -2
  8. package/dist/backend/FlashMessages.js +1 -25
  9. package/dist/backend/FlashMessages.js.map +2 -2
  10. package/dist/backend/columns/ColumnChooserPanel.js +3 -4
  11. package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
  12. package/dist/backend/confirm-dialog/ConfirmDialog.js +0 -11
  13. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  14. package/dist/backend/detail/InlineEditors.js +6 -17
  15. package/dist/backend/detail/InlineEditors.js.map +2 -2
  16. package/dist/backend/filters/FilterFieldPicker.js +2 -3
  17. package/dist/backend/filters/FilterFieldPicker.js.map +2 -2
  18. package/dist/backend/forms/ActionsDropdown.js +2 -52
  19. package/dist/backend/forms/ActionsDropdown.js.map +2 -2
  20. package/dist/backend/inputs/PhoneNumberField.js +5 -17
  21. package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
  22. package/dist/backend/inputs/TagsInput.js +1 -4
  23. package/dist/backend/inputs/TagsInput.js.map +2 -2
  24. package/dist/backend/inputs/index.js +0 -2
  25. package/dist/backend/inputs/index.js.map +2 -2
  26. package/dist/backend/messages/useMessageCompose.js +10 -29
  27. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  28. package/dist/backend/notifications/NotificationDispatcher.js +0 -5
  29. package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
  30. package/dist/backend/notifications/useNotificationActions.js +2 -13
  31. package/dist/backend/notifications/useNotificationActions.js.map +2 -2
  32. package/dist/backend/utils/api.js +4 -25
  33. package/dist/backend/utils/api.js.map +2 -2
  34. package/dist/backend/version-history/VersionHistoryPanel.js +2 -18
  35. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  36. package/dist/theme/QueryProvider.js +1 -7
  37. package/dist/theme/QueryProvider.js.map +2 -2
  38. package/package.json +4 -4
  39. package/src/ai/parts/MutationResultCard.tsx +1 -30
  40. package/src/ai/parts/__tests__/MutationResultCard.test.tsx +0 -48
  41. package/src/backend/CrudForm.tsx +60 -121
  42. package/src/backend/DataTable.tsx +14 -54
  43. package/src/backend/FlashMessages.tsx +1 -34
  44. package/src/backend/__tests__/ActionsDropdown.test.tsx +0 -105
  45. package/src/backend/__tests__/CrudForm.fieldInjection.test.tsx +4 -85
  46. package/src/backend/__tests__/FlashMessages.test.tsx +0 -65
  47. package/src/backend/__tests__/VersionHistoryPanel.test.tsx +1 -81
  48. package/src/backend/columns/ColumnChooserPanel.tsx +3 -4
  49. package/src/backend/confirm-dialog/ConfirmDialog.tsx +0 -16
  50. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +0 -29
  51. package/src/backend/detail/InlineEditors.tsx +6 -28
  52. package/src/backend/detail/__tests__/InlineEditors.test.tsx +6 -121
  53. package/src/backend/filters/FilterFieldPicker.tsx +2 -3
  54. package/src/backend/forms/ActionsDropdown.tsx +2 -71
  55. package/src/backend/inputs/PhoneNumberField.tsx +11 -17
  56. package/src/backend/inputs/TagsInput.tsx +0 -4
  57. package/src/backend/inputs/__tests__/TagsInput.test.tsx +0 -41
  58. package/src/backend/inputs/index.ts +0 -2
  59. package/src/backend/messages/__tests__/useMessageCompose.test.tsx +1 -106
  60. package/src/backend/messages/message-composer.types.ts +0 -2
  61. package/src/backend/messages/useMessageCompose.ts +13 -34
  62. package/src/backend/notifications/NotificationDispatcher.ts +0 -5
  63. package/src/backend/notifications/__tests__/useNotificationActions.test.tsx +1 -86
  64. package/src/backend/notifications/useNotificationActions.ts +4 -19
  65. package/src/backend/utils/__tests__/api.test.ts +0 -67
  66. package/src/backend/utils/api.ts +4 -33
  67. package/src/backend/version-history/VersionHistoryPanel.tsx +2 -24
  68. package/src/theme/QueryProvider.tsx +1 -7
  69. package/dist/backend/crud/injectedFieldGroups.js +0 -50
  70. package/dist/backend/crud/injectedFieldGroups.js.map +0 -7
  71. package/dist/backend/inputs/LookupPickers.js +0 -149
  72. package/dist/backend/inputs/LookupPickers.js.map +0 -7
  73. package/dist/backend/inputs/lookupSources.js +0 -84
  74. package/dist/backend/inputs/lookupSources.js.map +0 -7
  75. package/dist/backend/utils/groupLabels.js +0 -16
  76. package/dist/backend/utils/groupLabels.js.map +0 -7
  77. package/src/backend/__tests__/CrudForm.disabledFields.test.tsx +0 -57
  78. package/src/backend/__tests__/CrudForm.embeddedExtraActions.test.tsx +0 -114
  79. package/src/backend/__tests__/CrudForm.fieldChangeRenders.test.tsx +0 -127
  80. package/src/backend/__tests__/DataTable.injectedColumnPlacement.test.tsx +0 -146
  81. package/src/backend/__tests__/next-dynamic-specifier.test.ts +0 -23
  82. package/src/backend/columns/__tests__/ColumnChooserPanel.groupLabels.test.tsx +0 -72
  83. package/src/backend/crud/__tests__/injectedFieldGroups.test.ts +0 -90
  84. package/src/backend/crud/injectedFieldGroups.ts +0 -75
  85. package/src/backend/filters/__tests__/FilterFieldPicker.groupLabels.test.tsx +0 -77
  86. package/src/backend/inputs/LookupPickers.tsx +0 -181
  87. package/src/backend/inputs/__tests__/LookupPickers.test.tsx +0 -120
  88. package/src/backend/inputs/__tests__/PhoneNumberField.renders.test.tsx +0 -35
  89. package/src/backend/inputs/__tests__/lookupSources.test.ts +0 -87
  90. package/src/backend/inputs/lookupSources.ts +0 -114
  91. package/src/backend/notifications/__tests__/NotificationDispatcher.navigate.test.ts +0 -76
  92. package/src/backend/utils/groupLabels.ts +0 -26
  93. package/src/theme/__tests__/QueryProvider.i18n.test.tsx +0 -40
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/messages/useMessageCompose.ts"],
4
- "sourcesContent": ["import * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '../FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '../utils/apiCall'\nimport { buildOptimisticLockHeader } from '../utils/optimisticLock'\nimport { surfaceRecordConflict } from '../conflicts'\nimport type {\n AttachmentListResponse,\n MessageComposerProps,\n MessageSenderOption,\n MessageTypeItem,\n UserListItem,\n} from './message-composer.types'\nimport type { MessagePriority } from './message-priority'\nimport type { TagsInputOption } from '../inputs/TagsInput'\nimport {\n useComposeDraftOperation,\n useComposeSendOperation,\n useForwardSubmitOperation,\n useReplySubmitOperation,\n useSendDraftOperation,\n useUpdateDraftOperation,\n} from './useMessageComposeOperations'\n\nconst ATTACHMENT_LIST_PAGE_SIZE = 100\n\nfunction 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\n/**\n * Field-level errors a route returns alongside `error`, e.g. the send-as-user\n * facade's 422 for a mailbox that needs reconnecting. Read defensively: the\n * key is optional and any non-string value is dropped rather than rendered.\n */\nfunction readFieldErrors(payload: unknown): Record<string, string> | null {\n if (!payload || typeof payload !== 'object') return null\n const raw = (payload as Record<string, unknown>).fieldErrors\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const entries = Object.entries(raw as Record<string, unknown>)\n .filter((entry): entry is [string, string] => typeof entry[1] === 'string')\n return entries.length ? Object.fromEntries(entries) : null\n}\n\nfunction createTemporaryAttachmentRecordId(): string {\n const randomPart =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`\n return `messages-composer:${randomPart}`\n}\n\nexport type UseMessageComposeParams = MessageComposerProps\n\nexport type UseMessageComposeResult = {\n t: ReturnType<typeof useT>\n variant: NonNullable<MessageComposerProps['variant']>\n messageId?: string\n open?: boolean\n inline: boolean\n contextPreview: React.ReactNode\n isOpen: boolean\n messageTypes: MessageTypeItem[]\n createableMessageTypes: MessageTypeItem[]\n normalizedRequiredActionMode: 'none' | 'optional' | 'required'\n contextActionOptions: Array<{ id: string; label: string }>\n shouldShowContextActions: boolean\n isComposePublicVisibility: boolean\n attachmentEntityId: string\n attachmentRecordId: string\n recipientIds: string[]\n setRecipientIds: React.Dispatch<React.SetStateAction<string[]>>\n messageType: string\n setMessageType: React.Dispatch<React.SetStateAction<string>>\n subject: string\n setSubject: React.Dispatch<React.SetStateAction<string>>\n body: string\n setBody: React.Dispatch<React.SetStateAction<string>>\n bodyFormat: 'text' | 'markdown'\n setBodyFormat: React.Dispatch<React.SetStateAction<'text' | 'markdown'>>\n priority: MessagePriority\n setPriority: React.Dispatch<React.SetStateAction<MessagePriority>>\n visibility: 'public' | 'internal'\n setVisibility: React.Dispatch<React.SetStateAction<'public' | 'internal'>>\n externalEmail: string\n setExternalEmail: React.Dispatch<React.SetStateAction<string>>\n senderOptions: MessageSenderOption[]\n senderChannelId: string\n setSenderChannelId: React.Dispatch<React.SetStateAction<string>>\n sendViaEmail: boolean\n setSendViaEmail: React.Dispatch<React.SetStateAction<boolean>>\n contextActionRequired: boolean\n setContextActionRequired: React.Dispatch<React.SetStateAction<boolean>>\n contextActionType: string\n setContextActionType: React.Dispatch<React.SetStateAction<string>>\n replyAll: boolean\n setReplyAll: React.Dispatch<React.SetStateAction<boolean>>\n includeAttachments: boolean\n setIncludeAttachments: React.Dispatch<React.SetStateAction<boolean>>\n submitting: boolean\n submitMode: 'send' | 'draft'\n submitError: string | null\n submitFieldErrors: Record<string, string> | null\n composerTitle: string\n submitLabel: string\n selectedRecipientOptions: TagsInputOption[]\n resolveRecipientLabel: (id: string) => string\n loadRecipientSuggestions: (query?: string) => Promise<TagsInputOption[]>\n loadAttachmentIds: () => Promise<string[]>\n handleSaveDraft: () => void\n handleBack: () => void\n handleSubmit: ({ saveAsDraft }?: { saveAsDraft?: boolean }) => Promise<boolean>\n handleDialogOpenChange: (nextOpen: boolean) => void\n handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void\n}\n\ntype ForwardPreviewResponse = {\n subject?: string\n body?: string\n}\n\nexport function useMessageCompose({\n variant: variantProp = 'compose',\n messageId,\n open,\n onOpenChange,\n inline = false,\n lockedType = null,\n contextObject = null,\n requiredActionConfig = null,\n contextPreview = null,\n senderOptions,\n defaultValues,\n expectedUpdatedAt = null,\n onSuccess,\n onCancel,\n}: UseMessageComposeParams): UseMessageComposeResult {\n const t = useT()\n const variant = variantProp\n const isOpen = inline ? true : Boolean(open)\n const recipientSuggestionsCacheRef = React.useRef<TagsInputOption[] | null>(null)\n\n const [recipientIds, setRecipientIds] = React.useState<string[]>([])\n const [recipientMap, setRecipientMap] = React.useState<Record<string, TagsInputOption>>({})\n const [messageType, setMessageType] = React.useState(lockedType ?? 'default')\n const [subject, setSubject] = React.useState('')\n const [body, setBody] = React.useState('')\n const [bodyFormat, setBodyFormat] = React.useState<'text' | 'markdown'>('text')\n const [priority, setPriority] = React.useState<MessagePriority>('normal')\n const [visibility, setVisibility] = React.useState<'public' | 'internal'>('internal')\n const [externalEmail, setExternalEmail] = React.useState('')\n const [attachmentIds, setAttachmentIds] = React.useState<string[]>([])\n const [sendViaEmail, setSendViaEmail] = React.useState(false)\n const [contextActionRequired, setContextActionRequired] = React.useState(false)\n const [contextActionType, setContextActionType] = React.useState('')\n const [replyAll, setReplyAll] = React.useState(false)\n const [includeAttachments, setIncludeAttachments] = React.useState(true)\n const [temporaryAttachmentRecordId, setTemporaryAttachmentRecordId] = React.useState<string>(() =>\n createTemporaryAttachmentRecordId(),\n )\n const [submitting, setSubmitting] = React.useState(false)\n const [submitMode, setSubmitMode] = React.useState<'send' | 'draft'>('send')\n const [submitError, setSubmitError] = React.useState<string | null>(null)\n const [submitFieldErrors, setSubmitFieldErrors] = React.useState<Record<string, string> | null>(null)\n // Empty means the platform sender: the default the composer has always used.\n const [senderChannelId, setSenderChannelId] = React.useState('')\n // Tracks whether the composer is currently in the \"open\" lifecycle so the init\n // effect below only runs on the closed \u2192 open transition, not on every parent\n // re-render that produces a new `defaultValues` / `contextObject` reference\n // while the user is typing. Without this guard, an inline literal\n // `defaultValues={{...}}` in a re-rendering parent (e.g. message detail page\n // with live notification badges or queue progress) would clear the body /\n // subject mid-keystroke. CI shard 9 surfaced this as TC-MSG-009 timing out\n // because `keyboard.type` characters appeared to \"type nowhere\" \u2014 they were\n // typed correctly, then immediately wiped by the next effect run.\n const isOpenRef = React.useRef(false)\n const wasOpenRef = React.useRef(false)\n const submitLockReleaseRef = React.useRef<(() => void) | null>(null)\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types'],\n enabled: variant === 'compose' && isOpen,\n staleTime: 5 * 60 * 1000,\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 const messageTypes = React.useMemo(\n () => messageTypesQuery.data ?? [],\n [messageTypesQuery.data],\n )\n const createableMessageTypes = React.useMemo(\n () => messageTypes.filter((item) => item.isCreateableByUser !== false),\n [messageTypes],\n )\n const normalizedRequiredActionMode = requiredActionConfig?.mode ?? 'none'\n const contextActionOptions = React.useMemo(\n () => (requiredActionConfig?.options ?? []).filter((option) => option.id.trim().length > 0),\n [requiredActionConfig?.options],\n )\n const shouldShowContextActions = (\n variant === 'compose'\n && Boolean(contextObject)\n && normalizedRequiredActionMode !== 'none'\n && contextActionOptions.length > 0\n )\n\n const isComposePublicVisibility = variant === 'compose' && visibility === 'public'\n\n const attachmentEntityId = variant === 'compose' && messageId ? 'messages:message' : 'attachments:library'\n const attachmentRecordId = variant === 'compose' && messageId ? messageId : temporaryAttachmentRecordId\n\n const loadAttachmentIds = React.useCallback(async (): Promise<string[]> => {\n const collectedIds = new Set<string>()\n let expectedTotal: number | null = null\n let page = 1\n let hasMorePages = true\n\n while (hasMorePages) {\n const params = new URLSearchParams()\n params.set('entityId', attachmentEntityId)\n params.set('recordId', attachmentRecordId)\n params.set('page', String(page))\n params.set('pageSize', String(ATTACHMENT_LIST_PAGE_SIZE))\n\n const call = await apiCall<AttachmentListResponse>(`/api/attachments?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadAttachmentOptionsFailed', 'Failed to load attachments.'),\n )\n }\n\n const items = Array.isArray(call.result?.items) ? call.result.items : []\n for (const item of items) {\n if (typeof item?.id === 'string' && item.id.length > 0) collectedIds.add(item.id)\n }\n\n if (page === 1 && typeof call.result?.total === 'number') expectedTotal = call.result.total\n const totalPages = typeof call.result?.totalPages === 'number' ? call.result.totalPages : page\n hasMorePages = items.length === ATTACHMENT_LIST_PAGE_SIZE && page < totalPages\n page += 1\n }\n\n if (expectedTotal !== null && collectedIds.size < expectedTotal) {\n throw new Error(t('messages.errors.loadAttachmentOptionsFailed', 'Failed to load attachments.'))\n }\n\n const nextIds = Array.from(collectedIds)\n setAttachmentIds(nextIds)\n return nextIds\n }, [attachmentEntityId, attachmentRecordId, t])\n\n React.useEffect(() => {\n if (!isOpen) {\n isOpenRef.current = false\n return\n }\n // Only initialize on the closed \u2192 open transition. Subsequent parent\n // re-renders that change `defaultValues` / `contextObject` references\n // (inline object literals are a new reference on every render) MUST NOT\n // overwrite state the user has typed in.\n if (isOpenRef.current) return\n isOpenRef.current = true\n\n const nextRecipients = defaultValues?.recipients?.filter((value) => typeof value === 'string' && value.trim().length > 0) ?? []\n const dedupedRecipients = Array.from(new Set(nextRecipients))\n\n setRecipientIds(dedupedRecipients)\n setMessageType(lockedType ?? defaultValues?.type ?? 'default')\n setSubject(defaultValues?.subject ?? '')\n setBody(defaultValues?.body ?? '')\n setBodyFormat(defaultValues?.bodyFormat ?? 'text')\n setPriority(defaultValues?.priority ?? 'normal')\n setVisibility(defaultValues?.visibility ?? 'internal')\n setExternalEmail(defaultValues?.externalEmail ?? '')\n setAttachmentIds(\n Array.isArray(defaultValues?.attachmentIds)\n ? defaultValues.attachmentIds.filter((id): id is string => typeof id === 'string' && id.trim().length > 0)\n : [],\n )\n setSendViaEmail(Boolean(defaultValues?.sendViaEmail))\n if (contextObject) {\n const defaultContextActionType = requiredActionConfig?.defaultActionType?.trim() ?? ''\n const fallbackContextActionType = contextObject.actionType?.trim() ?? ''\n const selectedActionType = defaultContextActionType || fallbackContextActionType\n const selectedActionAllowed = contextActionOptions.some((option) => option.id === selectedActionType)\n const nextActionType = selectedActionAllowed ? selectedActionType : ''\n setContextActionType(nextActionType)\n if (normalizedRequiredActionMode === 'required') {\n setContextActionRequired(true)\n } else if (normalizedRequiredActionMode === 'optional') {\n setContextActionRequired(Boolean(nextActionType) || Boolean(contextObject.actionRequired))\n } else {\n setContextActionRequired(Boolean(contextObject.actionRequired))\n }\n } else {\n setContextActionType('')\n setContextActionRequired(false)\n }\n setReplyAll(Boolean(defaultValues?.replyAll))\n setIncludeAttachments(defaultValues?.includeAttachments !== false)\n setTemporaryAttachmentRecordId(createTemporaryAttachmentRecordId())\n setSubmitError(null)\n }, [\n contextActionOptions,\n contextObject,\n defaultValues,\n isOpen,\n lockedType,\n normalizedRequiredActionMode,\n requiredActionConfig?.defaultActionType,\n ])\n\n React.useEffect(() => {\n if (!isOpen) {\n submitLockReleaseRef.current?.()\n submitLockReleaseRef.current = null\n wasOpenRef.current = false\n return\n }\n\n const justOpened = isOpen && !wasOpenRef.current\n wasOpenRef.current = isOpen\n if (!justOpened) return\n submitLockReleaseRef.current?.()\n submitLockReleaseRef.current = null\n setSubmitting(false)\n setSubmitMode('send')\n }, [isOpen])\n\n React.useEffect(() => () => {\n submitLockReleaseRef.current?.()\n submitLockReleaseRef.current = null\n }, [])\n\n React.useEffect(() => {\n if (!isOpen) return\n if (variant !== 'forward') return\n if (!messageId) return\n\n let isActive = true\n\n void (async () => {\n const call = await apiCall<ForwardPreviewResponse>(`/api/messages/${encodeURIComponent(messageId)}/forward-preview`)\n if (!isActive) return\n\n if (!call.ok) {\n const message = toErrorMessage(call.result)\n ?? t('messages.errors.forwardPreviewFailed', 'Failed to load forward preview.')\n setSubmitError(message)\n flash(message, 'error')\n return\n }\n\n if (typeof call.result?.subject === 'string') {\n setSubject((previousValue) => (previousValue.trim().length > 0 ? previousValue : call.result?.subject ?? ''))\n }\n if (typeof call.result?.body === 'string') {\n setBody((previousValue) => (previousValue.trim().length > 0 ? previousValue : call.result?.body ?? ''))\n }\n setBodyFormat('text')\n })().catch((error) => {\n if (!isActive) return\n const message = error instanceof Error\n ? error.message\n : t('messages.errors.forwardPreviewFailed', 'Failed to load forward preview.')\n setSubmitError(message)\n flash(message, 'error')\n })\n\n return () => {\n isActive = false\n }\n }, [isOpen, messageId, t, variant])\n\n React.useEffect(() => {\n if (!isOpen) return\n if (variant !== 'compose' && variant !== 'reply') return\n void loadAttachmentIds().catch(() => null)\n }, [isOpen, loadAttachmentIds, variant])\n\n React.useEffect(() => {\n if (variant !== 'compose') return\n if (!createableMessageTypes.length) return\n\n if (lockedType) {\n if (createableMessageTypes.some((item) => item.type === lockedType)) {\n setMessageType(lockedType)\n return\n }\n const defaultType = createableMessageTypes.find((item) => item.type === 'default')\n setMessageType(defaultType?.type ?? createableMessageTypes[0]?.type ?? 'default')\n return\n }\n\n if (createableMessageTypes.some((item) => item.type === messageType)) return\n\n const defaultType = createableMessageTypes.find((item) => item.type === 'default')\n setMessageType(defaultType?.type ?? createableMessageTypes[0]?.type ?? 'default')\n }, [createableMessageTypes, lockedType, messageType, variant])\n\n React.useEffect(() => {\n if (variant !== 'compose') return\n if (visibility !== 'public') return\n setSendViaEmail(true)\n setRecipientIds([])\n }, [variant, visibility])\n\n React.useEffect(() => {\n if (isOpen) return\n recipientSuggestionsCacheRef.current = null\n }, [isOpen])\n\n const resolveRecipientLabel = React.useCallback((id: string) => {\n return recipientMap[id]?.label ?? id\n }, [recipientMap])\n\n const selectedRecipientOptions = React.useMemo(() => {\n return recipientIds.map((id) => recipientMap[id] ?? { value: id, label: id })\n }, [recipientIds, recipientMap])\n\n const loadRecipientSuggestions = React.useCallback(async (_query?: string) => {\n const cachedOptions = recipientSuggestionsCacheRef.current\n if (cachedOptions) {\n return cachedOptions\n }\n\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '100')\n // Scope suggestions to the composer's active organization: a message is stamped with\n // that org and its detail endpoint denies cross-org recipients, so a recipient from\n // another org could never open what they were sent.\n params.set('scopeToActiveOrganization', '1')\n // Recipient lookup is filtered in TagsInput because incremental auth user search is unreliable.\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) {\n return []\n }\n if (!call.ok) {\n return []\n }\n\n const rawItems = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n const options: TagsInputOption[] = []\n for (const item of rawItems) {\n if (!item || typeof item !== 'object') continue\n const id = typeof item.id === 'string' ? item.id : ''\n if (!id) continue\n\n const email = typeof item.email === 'string' && item.email.trim().length ? item.email.trim() : id\n const name = typeof item.name === 'string' && item.name.trim().length ? item.name.trim() : undefined\n\n options.push({\n value: id,\n label: email,\n description: name,\n })\n }\n\n if (options.length) {\n setRecipientMap((prev) => {\n const next = { ...prev }\n for (const option of options) {\n next[option.value] = option\n }\n return next\n })\n }\n\n recipientSuggestionsCacheRef.current = options\n return options\n }, [])\n\n const handleCancel = React.useCallback(() => {\n if (submitting) return\n if (!inline) {\n onOpenChange?.(false)\n }\n onCancel?.()\n }, [inline, onCancel, onOpenChange, submitting])\n\n // The \"Send from\" control only exists for a compose addressing an external\n // recipient, so a selection made before the mode changed must not survive into\n // a payload the control is no longer shown for.\n const resolvedSenderOptions = React.useMemo(\n () => (isComposePublicVisibility ? senderOptions ?? [] : []),\n [isComposePublicVisibility, senderOptions],\n )\n const effectiveSenderChannelId = resolvedSenderOptions.some((option) => option.id === senderChannelId)\n ? senderChannelId\n : ''\n\n const composeSendOperation = useComposeSendOperation({\n senderChannelId: effectiveSenderChannelId,\n t,\n messageType,\n createableMessageTypes,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const composeDraftOperation = useComposeDraftOperation({\n t,\n senderChannelId: effectiveSenderChannelId,\n messageId,\n messageType,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const replyOperation = useReplySubmitOperation({\n t,\n messageId,\n body,\n bodyFormat,\n replyAll,\n recipientIds,\n sendViaEmail,\n })\n\n const forwardOperation = useForwardSubmitOperation({\n t,\n messageId,\n recipientIds,\n body,\n includeAttachments,\n sendViaEmail,\n })\n\n const sendDraftOperation = useSendDraftOperation({\n t,\n senderChannelId: effectiveSenderChannelId,\n messageId: messageId ?? '',\n messageType,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const updateDraftOperation = useUpdateDraftOperation({\n t,\n senderChannelId: effectiveSenderChannelId,\n messageId: messageId ?? '',\n messageType,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const handleSubmit = React.useCallback(async ({ saveAsDraft = false }: { saveAsDraft?: boolean } = {}) => {\n if (submitting) return false\n\n setSubmitError(null)\n setSubmitFieldErrors(null)\n\n const isEditingExistingDraft = variant === 'compose' && Boolean(messageId)\n const isComposeDraftSubmit = saveAsDraft && variant === 'compose'\n const operation = isComposeDraftSubmit\n ? (isEditingExistingDraft ? updateDraftOperation : composeDraftOperation)\n : variant === 'compose'\n ? (isEditingExistingDraft ? sendDraftOperation : composeSendOperation)\n : variant === 'reply'\n ? replyOperation\n : forwardOperation\n\n const validationMessage = operation.validate()\n if (validationMessage) {\n setSubmitError(validationMessage)\n flash(validationMessage, 'error')\n return false\n }\n\n setSubmitMode(isComposeDraftSubmit ? 'draft' : 'send')\n setSubmitting(true)\n let keepSubmitLock = false\n let shouldReturnFalse = false\n\n try {\n let nextAttachmentIds = attachmentIds\n if (operation.requiresAttachmentRefresh) {\n try {\n nextAttachmentIds = await loadAttachmentIds()\n } catch (error) {\n const message = error instanceof Error\n ? error.message\n : t('messages.errors.loadAttachmentOptionsFailed', 'Failed to load attachments.')\n setSubmitError(message)\n flash(message, 'error')\n shouldReturnFalse = true\n }\n }\n\n if (!shouldReturnFalse) {\n const { endpoint, method, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })\n\n // Editing an existing draft is a PATCH on the message aggregate, so carry\n // the OSS optimistic-lock header to reject a stale overwrite. New\n // compose/reply/forward writes create fresh records and never lock.\n const lockHeaders = isEditingExistingDraft\n ? buildOptimisticLockHeader(expectedUpdatedAt)\n : {}\n\n const call = await withScopedApiRequestHeaders(lockHeaders, () =>\n apiCall<{ id?: string }>(endpoint, {\n method,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }),\n )\n\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) {\n setSubmitError(\n t('ui.forms.flash.recordModified', 'This record was modified by someone else. Refresh and try again.'),\n )\n // The shared conflict banner renders at page level, so it is hidden\n // behind the compose modal. Close the dialog to reveal it (with its\n // Refresh action); the stale draft must be reloaded anyway (#3260 QA).\n if (!inline) {\n onOpenChange?.(false)\n }\n } else {\n const message = toErrorMessage(call.result) ?? t('messages.errors.sendFailed', 'Failed to send message.')\n setSubmitError(message)\n setSubmitFieldErrors(readFieldErrors(call.result))\n flash(message, 'error')\n }\n shouldReturnFalse = true\n } else {\n flash(operation.successMessage, 'success')\n keepSubmitLock = true\n\n onSuccess?.({ id: call.result?.id })\n\n if (!inline) {\n onOpenChange?.(false)\n }\n }\n }\n } catch (error) {\n const message = error instanceof Error\n ? error.message\n : t('messages.errors.sendFailed', 'Failed to send message.')\n setSubmitError(message)\n flash(message, 'error')\n shouldReturnFalse = true\n } finally {\n if (!keepSubmitLock) {\n setSubmitting(false)\n }\n setSubmitMode('send')\n }\n\n if (shouldReturnFalse) {\n return false\n }\n\n if (keepSubmitLock) {\n return await new Promise<boolean>((resolve) => {\n submitLockReleaseRef.current = () => resolve(true)\n })\n }\n\n return true\n }, [\n attachmentIds,\n composeDraftOperation,\n composeSendOperation,\n expectedUpdatedAt,\n forwardOperation,\n inline,\n loadAttachmentIds,\n messageId,\n onOpenChange,\n onSuccess,\n replyOperation,\n sendDraftOperation,\n submitting,\n t,\n updateDraftOperation,\n variant,\n ])\n\n const handleSaveDraft = React.useCallback(() => {\n if (variant !== 'compose') return\n void handleSubmit({ saveAsDraft: true })\n }, [handleSubmit, variant])\n\n const handleBack = React.useCallback(() => {\n if (submitting) return\n handleCancel()\n }, [handleCancel, submitting])\n\n const handleDialogOpenChange = React.useCallback((nextOpen: boolean) => {\n if (nextOpen) {\n onOpenChange?.(true)\n return\n }\n void handleBack()\n }, [handleBack, onOpenChange])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n return\n }\n\n if (event.key === 'Escape') {\n if (event.defaultPrevented) return\n if (event.target instanceof Node && !event.currentTarget.contains(event.target)) return\n event.preventDefault()\n handleCancel()\n }\n }, [handleCancel, handleSubmit])\n\n const composerTitle = variant === 'reply'\n ? t('messages.reply', 'Reply')\n : variant === 'forward'\n ? t('messages.forward', 'Forward')\n : t('messages.compose', 'Compose message')\n\n const submitLabel = submitting\n ? submitMode === 'draft'\n ? t('messages.savingDraft', 'Saving draft...')\n : t('messages.sending', 'Sending...')\n : variant === 'reply'\n ? t('messages.reply', 'Reply')\n : variant === 'forward'\n ? t('messages.forward', 'Forward')\n : t('messages.send', 'Send')\n\n return {\n t,\n variant,\n messageId,\n open,\n inline,\n contextPreview,\n isOpen,\n messageTypes,\n createableMessageTypes,\n normalizedRequiredActionMode,\n contextActionOptions,\n shouldShowContextActions,\n isComposePublicVisibility,\n attachmentEntityId,\n attachmentRecordId,\n recipientIds,\n setRecipientIds,\n messageType,\n setMessageType,\n subject,\n setSubject,\n body,\n setBody,\n bodyFormat,\n setBodyFormat,\n priority,\n setPriority,\n visibility,\n setVisibility,\n externalEmail,\n setExternalEmail,\n senderOptions: resolvedSenderOptions,\n senderChannelId: effectiveSenderChannelId,\n setSenderChannelId,\n sendViaEmail,\n setSendViaEmail,\n contextActionRequired,\n setContextActionRequired,\n contextActionType,\n setContextActionType,\n replyAll,\n setReplyAll,\n includeAttachments,\n setIncludeAttachments,\n submitting,\n submitMode,\n submitError,\n submitFieldErrors,\n composerTitle,\n submitLabel,\n selectedRecipientOptions,\n resolveRecipientLabel,\n loadRecipientSuggestions,\n loadAttachmentIds,\n handleSaveDraft,\n handleBack,\n handleSubmit,\n handleDialogOpenChange,\n handleKeyDown,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '../FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '../utils/apiCall'\nimport { buildOptimisticLockHeader } from '../utils/optimisticLock'\nimport { surfaceRecordConflict } from '../conflicts'\nimport type {\n AttachmentListResponse,\n MessageComposerProps,\n MessageSenderOption,\n MessageTypeItem,\n UserListItem,\n} from './message-composer.types'\nimport type { MessagePriority } from './message-priority'\nimport type { TagsInputOption } from '../inputs/TagsInput'\nimport {\n useComposeDraftOperation,\n useComposeSendOperation,\n useForwardSubmitOperation,\n useReplySubmitOperation,\n useSendDraftOperation,\n useUpdateDraftOperation,\n} from './useMessageComposeOperations'\n\nfunction 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\n/**\n * Field-level errors a route returns alongside `error`, e.g. the send-as-user\n * facade's 422 for a mailbox that needs reconnecting. Read defensively: the\n * key is optional and any non-string value is dropped rather than rendered.\n */\nfunction readFieldErrors(payload: unknown): Record<string, string> | null {\n if (!payload || typeof payload !== 'object') return null\n const raw = (payload as Record<string, unknown>).fieldErrors\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const entries = Object.entries(raw as Record<string, unknown>)\n .filter((entry): entry is [string, string] => typeof entry[1] === 'string')\n return entries.length ? Object.fromEntries(entries) : null\n}\n\nfunction createTemporaryAttachmentRecordId(): string {\n const randomPart =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`\n return `messages-composer:${randomPart}`\n}\n\nexport type UseMessageComposeParams = MessageComposerProps\n\nexport type UseMessageComposeResult = {\n t: ReturnType<typeof useT>\n variant: NonNullable<MessageComposerProps['variant']>\n messageId?: string\n open?: boolean\n inline: boolean\n contextPreview: React.ReactNode\n isOpen: boolean\n messageTypes: MessageTypeItem[]\n createableMessageTypes: MessageTypeItem[]\n normalizedRequiredActionMode: 'none' | 'optional' | 'required'\n contextActionOptions: Array<{ id: string; label: string }>\n shouldShowContextActions: boolean\n isComposePublicVisibility: boolean\n attachmentEntityId: string\n attachmentRecordId: string\n recipientIds: string[]\n setRecipientIds: React.Dispatch<React.SetStateAction<string[]>>\n messageType: string\n setMessageType: React.Dispatch<React.SetStateAction<string>>\n subject: string\n setSubject: React.Dispatch<React.SetStateAction<string>>\n body: string\n setBody: React.Dispatch<React.SetStateAction<string>>\n bodyFormat: 'text' | 'markdown'\n setBodyFormat: React.Dispatch<React.SetStateAction<'text' | 'markdown'>>\n priority: MessagePriority\n setPriority: React.Dispatch<React.SetStateAction<MessagePriority>>\n visibility: 'public' | 'internal'\n setVisibility: React.Dispatch<React.SetStateAction<'public' | 'internal'>>\n externalEmail: string\n setExternalEmail: React.Dispatch<React.SetStateAction<string>>\n senderOptions: MessageSenderOption[]\n senderChannelId: string\n setSenderChannelId: React.Dispatch<React.SetStateAction<string>>\n sendViaEmail: boolean\n setSendViaEmail: React.Dispatch<React.SetStateAction<boolean>>\n contextActionRequired: boolean\n setContextActionRequired: React.Dispatch<React.SetStateAction<boolean>>\n contextActionType: string\n setContextActionType: React.Dispatch<React.SetStateAction<string>>\n replyAll: boolean\n setReplyAll: React.Dispatch<React.SetStateAction<boolean>>\n includeAttachments: boolean\n setIncludeAttachments: React.Dispatch<React.SetStateAction<boolean>>\n submitting: boolean\n submitMode: 'send' | 'draft'\n submitError: string | null\n submitFieldErrors: Record<string, string> | null\n composerTitle: string\n submitLabel: string\n selectedRecipientOptions: TagsInputOption[]\n resolveRecipientLabel: (id: string) => string\n loadRecipientSuggestions: (query?: string) => Promise<TagsInputOption[]>\n loadAttachmentIds: () => Promise<string[]>\n handleSaveDraft: () => void\n handleBack: () => void\n handleSubmit: ({ saveAsDraft }?: { saveAsDraft?: boolean }) => Promise<boolean>\n handleDialogOpenChange: (nextOpen: boolean) => void\n handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void\n}\n\ntype ForwardPreviewResponse = {\n subject?: string\n body?: string\n}\n\nexport function useMessageCompose({\n variant: variantProp = 'compose',\n messageId,\n open,\n onOpenChange,\n inline = false,\n lockedType = null,\n contextObject = null,\n requiredActionConfig = null,\n contextPreview = null,\n senderOptions,\n defaultValues,\n expectedUpdatedAt = null,\n onSuccess,\n onCancel,\n}: UseMessageComposeParams): UseMessageComposeResult {\n const t = useT()\n const variant = variantProp\n const isOpen = inline ? true : Boolean(open)\n const recipientSuggestionsCacheRef = React.useRef<TagsInputOption[] | null>(null)\n\n const [recipientIds, setRecipientIds] = React.useState<string[]>([])\n const [recipientMap, setRecipientMap] = React.useState<Record<string, TagsInputOption>>({})\n const [messageType, setMessageType] = React.useState(lockedType ?? 'default')\n const [subject, setSubject] = React.useState('')\n const [body, setBody] = React.useState('')\n const [bodyFormat, setBodyFormat] = React.useState<'text' | 'markdown'>('text')\n const [priority, setPriority] = React.useState<MessagePriority>('normal')\n const [visibility, setVisibility] = React.useState<'public' | 'internal'>('internal')\n const [externalEmail, setExternalEmail] = React.useState('')\n const [attachmentIds, setAttachmentIds] = React.useState<string[]>([])\n const [sendViaEmail, setSendViaEmail] = React.useState(false)\n const [contextActionRequired, setContextActionRequired] = React.useState(false)\n const [contextActionType, setContextActionType] = React.useState('')\n const [replyAll, setReplyAll] = React.useState(false)\n const [includeAttachments, setIncludeAttachments] = React.useState(true)\n const [temporaryAttachmentRecordId, setTemporaryAttachmentRecordId] = React.useState<string>(() =>\n createTemporaryAttachmentRecordId(),\n )\n const [submitting, setSubmitting] = React.useState(false)\n const [submitMode, setSubmitMode] = React.useState<'send' | 'draft'>('send')\n const [submitError, setSubmitError] = React.useState<string | null>(null)\n const [submitFieldErrors, setSubmitFieldErrors] = React.useState<Record<string, string> | null>(null)\n // Empty means the platform sender: the default the composer has always used.\n const [senderChannelId, setSenderChannelId] = React.useState('')\n // Tracks whether the composer is currently in the \"open\" lifecycle so the init\n // effect below only runs on the closed \u2192 open transition, not on every parent\n // re-render that produces a new `defaultValues` / `contextObject` reference\n // while the user is typing. Without this guard, an inline literal\n // `defaultValues={{...}}` in a re-rendering parent (e.g. message detail page\n // with live notification badges or queue progress) would clear the body /\n // subject mid-keystroke. CI shard 9 surfaced this as TC-MSG-009 timing out\n // because `keyboard.type` characters appeared to \"type nowhere\" \u2014 they were\n // typed correctly, then immediately wiped by the next effect run.\n const isOpenRef = React.useRef(false)\n const wasOpenRef = React.useRef(false)\n const submitLockReleaseRef = React.useRef<(() => void) | null>(null)\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types'],\n enabled: variant === 'compose' && isOpen,\n staleTime: 5 * 60 * 1000,\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 const messageTypes = React.useMemo(\n () => messageTypesQuery.data ?? [],\n [messageTypesQuery.data],\n )\n const createableMessageTypes = React.useMemo(\n () => messageTypes.filter((item) => item.isCreateableByUser !== false),\n [messageTypes],\n )\n const normalizedRequiredActionMode = requiredActionConfig?.mode ?? 'none'\n const contextActionOptions = React.useMemo(\n () => (requiredActionConfig?.options ?? []).filter((option) => option.id.trim().length > 0),\n [requiredActionConfig?.options],\n )\n const shouldShowContextActions = (\n variant === 'compose'\n && Boolean(contextObject)\n && normalizedRequiredActionMode !== 'none'\n && contextActionOptions.length > 0\n )\n\n const isComposePublicVisibility = variant === 'compose' && visibility === 'public'\n\n const attachmentEntityId = variant === 'compose' && messageId ? 'messages:message' : 'attachments:library'\n const attachmentRecordId = variant === 'compose' && messageId ? messageId : temporaryAttachmentRecordId\n\n const loadAttachmentIds = React.useCallback(async (): Promise<string[]> => {\n const params = new URLSearchParams()\n params.set('entityId', attachmentEntityId)\n params.set('recordId', attachmentRecordId)\n\n const call = await apiCall<AttachmentListResponse>(`/api/attachments?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadAttachmentOptionsFailed', 'Failed to load attachments.'),\n )\n }\n\n const items = Array.isArray(call.result?.items) ? call.result.items : []\n const nextIds = items\n .map((item) => (typeof item?.id === 'string' ? item.id : ''))\n .filter((id) => id.length > 0)\n\n setAttachmentIds(nextIds)\n return nextIds\n }, [attachmentEntityId, attachmentRecordId, t])\n\n React.useEffect(() => {\n if (!isOpen) {\n isOpenRef.current = false\n return\n }\n // Only initialize on the closed \u2192 open transition. Subsequent parent\n // re-renders that change `defaultValues` / `contextObject` references\n // (inline object literals are a new reference on every render) MUST NOT\n // overwrite state the user has typed in.\n if (isOpenRef.current) return\n isOpenRef.current = true\n\n const nextRecipients = defaultValues?.recipients?.filter((value) => typeof value === 'string' && value.trim().length > 0) ?? []\n const dedupedRecipients = Array.from(new Set(nextRecipients))\n\n setRecipientIds(dedupedRecipients)\n setMessageType(lockedType ?? defaultValues?.type ?? 'default')\n setSubject(defaultValues?.subject ?? '')\n setBody(defaultValues?.body ?? '')\n setBodyFormat(defaultValues?.bodyFormat ?? 'text')\n setPriority(defaultValues?.priority ?? 'normal')\n setVisibility(defaultValues?.visibility ?? 'internal')\n setExternalEmail(defaultValues?.externalEmail ?? '')\n setAttachmentIds(\n Array.isArray(defaultValues?.attachmentIds)\n ? defaultValues.attachmentIds.filter((id): id is string => typeof id === 'string' && id.trim().length > 0)\n : [],\n )\n setSendViaEmail(Boolean(defaultValues?.sendViaEmail))\n if (contextObject) {\n const defaultContextActionType = requiredActionConfig?.defaultActionType?.trim() ?? ''\n const fallbackContextActionType = contextObject.actionType?.trim() ?? ''\n const selectedActionType = defaultContextActionType || fallbackContextActionType\n const selectedActionAllowed = contextActionOptions.some((option) => option.id === selectedActionType)\n const nextActionType = selectedActionAllowed ? selectedActionType : ''\n setContextActionType(nextActionType)\n if (normalizedRequiredActionMode === 'required') {\n setContextActionRequired(true)\n } else if (normalizedRequiredActionMode === 'optional') {\n setContextActionRequired(Boolean(nextActionType) || Boolean(contextObject.actionRequired))\n } else {\n setContextActionRequired(Boolean(contextObject.actionRequired))\n }\n } else {\n setContextActionType('')\n setContextActionRequired(false)\n }\n setReplyAll(Boolean(defaultValues?.replyAll))\n setIncludeAttachments(defaultValues?.includeAttachments !== false)\n setTemporaryAttachmentRecordId(createTemporaryAttachmentRecordId())\n setSubmitError(null)\n }, [\n contextActionOptions,\n contextObject,\n defaultValues,\n isOpen,\n lockedType,\n normalizedRequiredActionMode,\n requiredActionConfig?.defaultActionType,\n ])\n\n React.useEffect(() => {\n if (!isOpen) {\n submitLockReleaseRef.current?.()\n submitLockReleaseRef.current = null\n wasOpenRef.current = false\n return\n }\n\n const justOpened = isOpen && !wasOpenRef.current\n wasOpenRef.current = isOpen\n if (!justOpened) return\n submitLockReleaseRef.current?.()\n submitLockReleaseRef.current = null\n setSubmitting(false)\n setSubmitMode('send')\n }, [isOpen])\n\n React.useEffect(() => () => {\n submitLockReleaseRef.current?.()\n submitLockReleaseRef.current = null\n }, [])\n\n React.useEffect(() => {\n if (!isOpen) return\n if (variant !== 'forward') return\n if (!messageId) return\n\n let isActive = true\n\n void (async () => {\n const call = await apiCall<ForwardPreviewResponse>(`/api/messages/${encodeURIComponent(messageId)}/forward-preview`)\n if (!isActive) return\n\n if (!call.ok) {\n const message = toErrorMessage(call.result)\n ?? t('messages.errors.forwardPreviewFailed', 'Failed to load forward preview.')\n setSubmitError(message)\n flash(message, 'error')\n return\n }\n\n if (typeof call.result?.subject === 'string') {\n setSubject((previousValue) => (previousValue.trim().length > 0 ? previousValue : call.result?.subject ?? ''))\n }\n if (typeof call.result?.body === 'string') {\n setBody((previousValue) => (previousValue.trim().length > 0 ? previousValue : call.result?.body ?? ''))\n }\n setBodyFormat('text')\n })().catch((error) => {\n if (!isActive) return\n const message = error instanceof Error\n ? error.message\n : t('messages.errors.forwardPreviewFailed', 'Failed to load forward preview.')\n setSubmitError(message)\n flash(message, 'error')\n })\n\n return () => {\n isActive = false\n }\n }, [isOpen, messageId, t, variant])\n\n React.useEffect(() => {\n if (!isOpen) return\n if (variant !== 'compose' && variant !== 'reply') return\n void loadAttachmentIds().catch(() => null)\n }, [isOpen, loadAttachmentIds, variant])\n\n React.useEffect(() => {\n if (variant !== 'compose') return\n if (!createableMessageTypes.length) return\n\n if (lockedType) {\n if (createableMessageTypes.some((item) => item.type === lockedType)) {\n setMessageType(lockedType)\n return\n }\n const defaultType = createableMessageTypes.find((item) => item.type === 'default')\n setMessageType(defaultType?.type ?? createableMessageTypes[0]?.type ?? 'default')\n return\n }\n\n if (createableMessageTypes.some((item) => item.type === messageType)) return\n\n const defaultType = createableMessageTypes.find((item) => item.type === 'default')\n setMessageType(defaultType?.type ?? createableMessageTypes[0]?.type ?? 'default')\n }, [createableMessageTypes, lockedType, messageType, variant])\n\n React.useEffect(() => {\n if (variant !== 'compose') return\n if (visibility !== 'public') return\n setSendViaEmail(true)\n setRecipientIds([])\n }, [variant, visibility])\n\n React.useEffect(() => {\n if (isOpen) return\n recipientSuggestionsCacheRef.current = null\n }, [isOpen])\n\n const resolveRecipientLabel = React.useCallback((id: string) => {\n return recipientMap[id]?.label ?? id\n }, [recipientMap])\n\n const selectedRecipientOptions = React.useMemo(() => {\n return recipientIds.map((id) => recipientMap[id] ?? { value: id, label: id })\n }, [recipientIds, recipientMap])\n\n const loadRecipientSuggestions = React.useCallback(async (_query?: string) => {\n const cachedOptions = recipientSuggestionsCacheRef.current\n if (cachedOptions) {\n return cachedOptions\n }\n\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '100')\n // Scope suggestions to the composer's active organization: a message is stamped with\n // that org and its detail endpoint denies cross-org recipients, so a recipient from\n // another org could never open what they were sent.\n params.set('scopeToActiveOrganization', '1')\n // Recipient lookup is filtered in TagsInput because incremental auth user search is unreliable.\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) {\n return []\n }\n if (!call.ok) {\n return []\n }\n\n const rawItems = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n const options: TagsInputOption[] = []\n for (const item of rawItems) {\n if (!item || typeof item !== 'object') continue\n const id = typeof item.id === 'string' ? item.id : ''\n if (!id) continue\n\n const email = typeof item.email === 'string' && item.email.trim().length ? item.email.trim() : id\n const name = typeof item.name === 'string' && item.name.trim().length ? item.name.trim() : undefined\n\n options.push({\n value: id,\n label: email,\n description: name,\n })\n }\n\n if (options.length) {\n setRecipientMap((prev) => {\n const next = { ...prev }\n for (const option of options) {\n next[option.value] = option\n }\n return next\n })\n }\n\n recipientSuggestionsCacheRef.current = options\n return options\n }, [])\n\n const handleCancel = React.useCallback(() => {\n if (submitting) return\n if (!inline) {\n onOpenChange?.(false)\n }\n onCancel?.()\n }, [inline, onCancel, onOpenChange, submitting])\n\n // The \"Send from\" control only exists for a compose addressing an external\n // recipient, so a selection made before the mode changed must not survive into\n // a payload the control is no longer shown for.\n const resolvedSenderOptions = React.useMemo(\n () => (isComposePublicVisibility ? senderOptions ?? [] : []),\n [isComposePublicVisibility, senderOptions],\n )\n const effectiveSenderChannelId = resolvedSenderOptions.some((option) => option.id === senderChannelId)\n ? senderChannelId\n : ''\n\n const composeSendOperation = useComposeSendOperation({\n senderChannelId: effectiveSenderChannelId,\n t,\n messageType,\n createableMessageTypes,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const composeDraftOperation = useComposeDraftOperation({\n t,\n senderChannelId: effectiveSenderChannelId,\n messageId,\n messageType,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const replyOperation = useReplySubmitOperation({\n t,\n messageId,\n body,\n bodyFormat,\n replyAll,\n recipientIds,\n sendViaEmail,\n })\n\n const forwardOperation = useForwardSubmitOperation({\n t,\n messageId,\n recipientIds,\n body,\n includeAttachments,\n sendViaEmail,\n })\n\n const sendDraftOperation = useSendDraftOperation({\n t,\n senderChannelId: effectiveSenderChannelId,\n messageId: messageId ?? '',\n messageType,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const updateDraftOperation = useUpdateDraftOperation({\n t,\n senderChannelId: effectiveSenderChannelId,\n messageId: messageId ?? '',\n messageType,\n priority,\n visibility,\n externalEmail,\n recipientIds,\n subject,\n body,\n bodyFormat,\n sendViaEmail,\n contextObject,\n defaultValues,\n contextActionOptions,\n normalizedRequiredActionMode,\n shouldShowContextActions,\n contextActionRequired,\n contextActionType,\n })\n\n const handleSubmit = React.useCallback(async ({ saveAsDraft = false }: { saveAsDraft?: boolean } = {}) => {\n if (submitting) return false\n\n setSubmitError(null)\n setSubmitFieldErrors(null)\n\n const isEditingExistingDraft = variant === 'compose' && Boolean(messageId)\n const isComposeDraftSubmit = saveAsDraft && variant === 'compose'\n const operation = isComposeDraftSubmit\n ? (isEditingExistingDraft ? updateDraftOperation : composeDraftOperation)\n : variant === 'compose'\n ? (isEditingExistingDraft ? sendDraftOperation : composeSendOperation)\n : variant === 'reply'\n ? replyOperation\n : forwardOperation\n\n const validationMessage = operation.validate()\n if (validationMessage) {\n setSubmitError(validationMessage)\n flash(validationMessage, 'error')\n return false\n }\n\n setSubmitMode(isComposeDraftSubmit ? 'draft' : 'send')\n setSubmitting(true)\n let keepSubmitLock = false\n let shouldReturnFalse = false\n\n try {\n let nextAttachmentIds = attachmentIds\n if (operation.requiresAttachmentRefresh) {\n try {\n nextAttachmentIds = await loadAttachmentIds()\n } catch (error) {\n const message = error instanceof Error\n ? error.message\n : t('messages.errors.loadAttachmentOptionsFailed', 'Failed to load attachments.')\n setSubmitError(message)\n flash(message, 'error')\n shouldReturnFalse = true\n }\n }\n\n if (!shouldReturnFalse) {\n const { endpoint, method, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })\n\n // Editing an existing draft is a PATCH on the message aggregate, so carry\n // the OSS optimistic-lock header to reject a stale overwrite. New\n // compose/reply/forward writes create fresh records and never lock.\n const lockHeaders = isEditingExistingDraft\n ? buildOptimisticLockHeader(expectedUpdatedAt)\n : {}\n\n const call = await withScopedApiRequestHeaders(lockHeaders, () =>\n apiCall<{ id?: string }>(endpoint, {\n method,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }),\n )\n\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) {\n setSubmitError(\n t('ui.forms.flash.recordModified', 'This record was modified by someone else. Refresh and try again.'),\n )\n // The shared conflict banner renders at page level, so it is hidden\n // behind the compose modal. Close the dialog to reveal it (with its\n // Refresh action); the stale draft must be reloaded anyway (#3260 QA).\n if (!inline) {\n onOpenChange?.(false)\n }\n } else {\n const message = toErrorMessage(call.result) ?? t('messages.errors.sendFailed', 'Failed to send message.')\n setSubmitError(message)\n setSubmitFieldErrors(readFieldErrors(call.result))\n flash(message, 'error')\n }\n shouldReturnFalse = true\n } else {\n flash(operation.successMessage, 'success')\n keepSubmitLock = true\n\n onSuccess?.({ id: call.result?.id })\n\n if (!inline) {\n onOpenChange?.(false)\n }\n }\n }\n } catch (error) {\n const message = error instanceof Error\n ? error.message\n : t('messages.errors.sendFailed', 'Failed to send message.')\n setSubmitError(message)\n flash(message, 'error')\n shouldReturnFalse = true\n } finally {\n if (!keepSubmitLock) {\n setSubmitting(false)\n }\n setSubmitMode('send')\n }\n\n if (shouldReturnFalse) {\n return false\n }\n\n if (keepSubmitLock) {\n return await new Promise<boolean>((resolve) => {\n submitLockReleaseRef.current = () => resolve(true)\n })\n }\n\n return true\n }, [\n attachmentIds,\n composeDraftOperation,\n composeSendOperation,\n expectedUpdatedAt,\n forwardOperation,\n inline,\n loadAttachmentIds,\n messageId,\n onOpenChange,\n onSuccess,\n replyOperation,\n sendDraftOperation,\n submitting,\n t,\n updateDraftOperation,\n variant,\n ])\n\n const handleSaveDraft = React.useCallback(() => {\n if (variant !== 'compose') return\n void handleSubmit({ saveAsDraft: true })\n }, [handleSubmit, variant])\n\n const handleBack = React.useCallback(() => {\n if (submitting) return\n handleCancel()\n }, [handleCancel, submitting])\n\n const handleDialogOpenChange = React.useCallback((nextOpen: boolean) => {\n if (nextOpen) {\n onOpenChange?.(true)\n return\n }\n void handleBack()\n }, [handleBack, onOpenChange])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n return\n }\n\n if (event.key === 'Escape') {\n if (event.defaultPrevented) return\n if (event.target instanceof Node && !event.currentTarget.contains(event.target)) return\n event.preventDefault()\n handleCancel()\n }\n }, [handleCancel, handleSubmit])\n\n const composerTitle = variant === 'reply'\n ? t('messages.reply', 'Reply')\n : variant === 'forward'\n ? t('messages.forward', 'Forward')\n : t('messages.compose', 'Compose message')\n\n const submitLabel = submitting\n ? submitMode === 'draft'\n ? t('messages.savingDraft', 'Saving draft...')\n : t('messages.sending', 'Sending...')\n : variant === 'reply'\n ? t('messages.reply', 'Reply')\n : variant === 'forward'\n ? t('messages.forward', 'Forward')\n : t('messages.send', 'Send')\n\n return {\n t,\n variant,\n messageId,\n open,\n inline,\n contextPreview,\n isOpen,\n messageTypes,\n createableMessageTypes,\n normalizedRequiredActionMode,\n contextActionOptions,\n shouldShowContextActions,\n isComposePublicVisibility,\n attachmentEntityId,\n attachmentRecordId,\n recipientIds,\n setRecipientIds,\n messageType,\n setMessageType,\n subject,\n setSubject,\n body,\n setBody,\n bodyFormat,\n setBodyFormat,\n priority,\n setPriority,\n visibility,\n setVisibility,\n externalEmail,\n setExternalEmail,\n senderOptions: resolvedSenderOptions,\n senderChannelId: effectiveSenderChannelId,\n setSenderChannelId,\n sendViaEmail,\n setSendViaEmail,\n contextActionRequired,\n setContextActionRequired,\n contextActionType,\n setContextActionType,\n replyAll,\n setReplyAll,\n includeAttachments,\n setIncludeAttachments,\n submitting,\n submitMode,\n submitError,\n submitFieldErrors,\n composerTitle,\n submitLabel,\n selectedRecipientOptions,\n resolveRecipientLabel,\n loadRecipientSuggestions,\n loadAttachmentIds,\n handleSaveDraft,\n handleBack,\n handleSubmit,\n handleDialogOpenChange,\n handleKeyDown,\n }\n}\n"],
5
+ "mappings": "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YACV,EAAE,oBAAoB,SAAS,IAC/B,EAAE,iBAAiB,MAAM;AAEjC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,iBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": ["defaultType"]
7
7
  }
@@ -3,7 +3,6 @@ import {
3
3
  getNotificationHandlerEntries
4
4
  } from "@open-mercato/shared/lib/notifications/handler-registry";
5
5
  import { hasAllFeatures } from "@open-mercato/shared/security/features";
6
- import { isSafeNotificationHref } from "@open-mercato/shared/modules/notifications/safeHref";
7
6
  import { createLogger } from "@open-mercato/shared/lib/logger";
8
7
  import { localizeNotificationCopy } from "./localizeNotification.js";
9
8
  const logger = createLogger("ui").child({ component: "notifications" });
@@ -51,10 +50,6 @@ function buildHandlerContext(runtime) {
51
50
  },
52
51
  refreshNotifications: runtime.refreshNotifications,
53
52
  navigate(href) {
54
- if (!isSafeNotificationHref(href)) {
55
- logger.warn("Notification handler navigation ignored: href is not a same-origin path");
56
- return;
57
- }
58
53
  runtime.navigate(href);
59
54
  },
60
55
  markAsRead: runtime.markAsRead,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/notifications/NotificationDispatcher.ts"],
4
- "sourcesContent": ["import { flash, type FlashKind } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n getNotificationHandlerEntries,\n} from '@open-mercato/shared/lib/notifications/handler-registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type {\n NotificationHandler,\n NotificationHandlerContext,\n NotificationHandlerPopupOptions,\n NotificationHandlerToastOptions,\n} from '@open-mercato/shared/modules/notifications/handler'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport { isSafeNotificationHref } from '@open-mercato/shared/modules/notifications/safeHref'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { localizeNotificationCopy } from './localizeNotification'\n\nconst logger = createLogger('ui').child({ component: 'notifications' })\n\ntype RuntimeContext = {\n userId?: string\n features: string[]\n currentPath: string\n t?: (key: string, fallback?: string) => string\n refreshNotifications: () => void\n navigate: (href: string) => void\n markAsRead: (notificationId: string) => Promise<void>\n dismiss: (notificationId: string) => Promise<void>\n}\n\ntype NotificationEffectListener = {\n id: number\n pattern: string | string[]\n effect: (notification: NotificationDto) => void\n}\n\nconst DEFAULT_PRIORITY = 50\nconst MAX_TRACKED_IDS = 500\nconst RETAINED_IDS_AFTER_PRUNE = 200\nconst POPUP_EVENT_NAME = 'om:notifications:popup'\n\nfunction toFlashKind(value: NotificationHandlerToastOptions['severity'] | NotificationHandlerPopupOptions['severity']): FlashKind {\n if (value === 'success') return 'success'\n if (value === 'warning') return 'warning'\n if (value === 'error') return 'error'\n return 'info'\n}\n\nfunction matchesType(pattern: string | string[], type: string): boolean {\n const patterns = Array.isArray(pattern) ? pattern : [pattern]\n return patterns.some((current) => {\n if (current === '*') return true\n if (current.endsWith('.*')) return type.startsWith(current.slice(0, -1))\n return current === type\n })\n}\n\nfunction matchesFeatures(required: string[] | undefined, current: string[]): boolean {\n return hasAllFeatures(current, required)\n}\n\nfunction buildHandlerContext(runtime: RuntimeContext): NotificationHandlerContext {\n return {\n userId: runtime.userId,\n features: runtime.features,\n currentPath: runtime.currentPath,\n t: runtime.t,\n toast(options) {\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n popup(options) {\n if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {\n window.dispatchEvent(new CustomEvent(POPUP_EVENT_NAME, { detail: options }))\n }\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n emitEvent(eventName, detail) {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(eventName, { detail }))\n },\n refreshNotifications: runtime.refreshNotifications,\n navigate(href) {\n if (!isSafeNotificationHref(href)) {\n logger.warn('Notification handler navigation ignored: href is not a same-origin path')\n return\n }\n runtime.navigate(href)\n },\n markAsRead: runtime.markAsRead,\n dismiss: runtime.dismiss,\n }\n}\n\nclass NotificationDispatcher {\n private handledIds = new Set<string>()\n private handledOrder: string[] = []\n private listeners = new Map<number, NotificationEffectListener>()\n private listenerIdSeq = 1\n private debounceByHandler = new Map<string, number>()\n\n dispatch(notifications: NotificationDto[], runtime: RuntimeContext) {\n const entries = getNotificationHandlerEntries()\n if (notifications.length === 0 && this.listeners.size === 0) return\n const context = buildHandlerContext(runtime)\n const sortedEntries = entries\n .slice()\n .sort((a, b) => (b.handler.priority ?? DEFAULT_PRIORITY) - (a.handler.priority ?? DEFAULT_PRIORITY))\n\n for (const notification of notifications) {\n if (this.handledIds.has(notification.id)) continue\n\n let handlerRan = false\n let hadFeatureBlockedHandler = false\n const localized = localizeNotificationCopy(notification, runtime.t)\n\n for (const entry of sortedEntries) {\n const handler = entry.handler\n if (!matchesType(handler.notificationType, notification.type)) continue\n if (!matchesFeatures(handler.features, runtime.features)) {\n hadFeatureBlockedHandler = true\n continue\n }\n if (this.shouldDebounce(handler, notification.id)) continue\n handlerRan = true\n try {\n void Promise.resolve(handler.handle(localized, context))\n } catch (error) {\n logger.error('Notification handler failed', { handlerId: handler.id, err: error })\n }\n }\n\n for (const listener of this.listeners.values()) {\n if (!matchesType(listener.pattern, notification.type)) continue\n handlerRan = true\n try {\n listener.effect(notification)\n } catch (error) {\n logger.error('Notification effect listener failed', { err: error })\n }\n }\n\n // Only mark as permanently handled when something actually ran, or when no handler\n // was blocked by missing features. This prevents a race where the initial notification\n // fetch completes before the feature-check API resolves (features: []), causing all\n // feature-gated handlers to be skipped \u2014 but the notification gets permanently\n // deduped so it never fires when features are available on the next dispatch.\n if (handlerRan || !hadFeatureBlockedHandler) {\n this.markHandled(notification.id)\n }\n }\n }\n\n subscribe(pattern: string | string[], effect: (notification: NotificationDto) => void): () => void {\n const id = this.listenerIdSeq++\n this.listeners.set(id, { id, pattern, effect })\n return () => {\n this.listeners.delete(id)\n }\n }\n\n resetForTests() {\n this.handledIds.clear()\n this.handledOrder = []\n this.listeners.clear()\n this.listenerIdSeq = 1\n this.debounceByHandler.clear()\n }\n\n private shouldDebounce(handler: NotificationHandler, notificationId: string): boolean {\n if (!handler.debounceMs || handler.debounceMs <= 0) return false\n const key = `${handler.id}:${notificationId}`\n const now = Date.now()\n const previous = this.debounceByHandler.get(key)\n if (typeof previous === 'number' && now - previous < handler.debounceMs) {\n return true\n }\n this.debounceByHandler.set(key, now)\n return false\n }\n\n private markHandled(notificationId: string) {\n this.handledIds.add(notificationId)\n this.handledOrder.push(notificationId)\n if (this.handledOrder.length <= MAX_TRACKED_IDS) return\n const removeCount = this.handledOrder.length - RETAINED_IDS_AFTER_PRUNE\n const removed = this.handledOrder.splice(0, removeCount)\n for (const id of removed) {\n this.handledIds.delete(id)\n }\n }\n}\n\nconst dispatcher = new NotificationDispatcher()\n\nexport function dispatchNotificationHandlers(\n notifications: NotificationDto[],\n runtime: RuntimeContext,\n) {\n dispatcher.dispatch(notifications, runtime)\n}\n\nexport function subscribeNotificationEffects(\n pattern: string | string[],\n effect: (notification: NotificationDto) => void,\n): () => void {\n return dispatcher.subscribe(pattern, effect)\n}\n\nexport function getRequiredNotificationHandlerFeatures(): string[] {\n const required = new Set<string>()\n const entries = getNotificationHandlerEntries()\n for (const entry of entries) {\n for (const feature of entry.handler.features ?? []) {\n if (feature && feature.trim().length > 0) {\n required.add(feature)\n }\n }\n }\n return Array.from(required)\n}\n\nexport function __resetNotificationDispatcherForTests() {\n dispatcher.resetForTests()\n}\n"],
5
- "mappings": "AAAA,SAAS,aAA6B;AACtC;AAAA,EACE;AAAA,OACK;AACP,SAAS,sBAAsB;AAQ/B,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AAEzC,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,gBAAgB,CAAC;AAmBtE,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AACxB,MAAM,2BAA2B;AACjC,MAAM,mBAAmB;AAEzB,SAAS,YAAY,OAA6G;AAChI,MAAI,UAAU,UAAW,QAAO;AAChC,MAAI,UAAU,UAAW,QAAO;AAChC,MAAI,UAAU,QAAS,QAAO;AAC9B,SAAO;AACT;AAEA,SAAS,YAAY,SAA4B,MAAuB;AACtE,QAAM,WAAW,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;AAC5D,SAAO,SAAS,KAAK,CAAC,YAAY;AAChC,QAAI,YAAY,IAAK,QAAO;AAC5B,QAAI,QAAQ,SAAS,IAAI,EAAG,QAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;AACvE,WAAO,YAAY;AAAA,EACrB,CAAC;AACH;AAEA,SAAS,gBAAgB,UAAgC,SAA4B;AACnF,SAAO,eAAe,SAAS,QAAQ;AACzC;AAEA,SAAS,oBAAoB,SAAqD;AAChF,SAAO;AAAA,IACL,QAAQ,QAAQ;AAAA,IAChB,UAAU,QAAQ;AAAA,IAClB,aAAa,QAAQ;AAAA,IACrB,GAAG,QAAQ;AAAA,IACX,MAAM,SAAS;AACb,YAAM,OAAO,QAAQ,OAAO,GAAG,QAAQ,KAAK,KAAK,QAAQ,IAAI,KAAK,QAAQ;AAC1E,YAAM,MAAM,YAAY,QAAQ,QAAQ,CAAC;AAAA,IAC3C;AAAA,IACA,MAAM,SAAS;AACb,UAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,aAAa;AACvE,eAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,QAAQ,CAAC,CAAC;AAAA,MAC7E;AACA,YAAM,OAAO,QAAQ,OAAO,GAAG,QAAQ,KAAK,KAAK,QAAQ,IAAI,KAAK,QAAQ;AAC1E,YAAM,MAAM,YAAY,QAAQ,QAAQ,CAAC;AAAA,IAC3C;AAAA,IACA,UAAU,WAAW,QAAQ;AAC3B,UAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AACzE,aAAO,cAAc,IAAI,YAAY,WAAW,EAAE,OAAO,CAAC,CAAC;AAAA,IAC7D;AAAA,IACA,sBAAsB,QAAQ;AAAA,IAC9B,SAAS,MAAM;AACb,UAAI,CAAC,uBAAuB,IAAI,GAAG;AACjC,eAAO,KAAK,yEAAyE;AACrF;AAAA,MACF;AACA,cAAQ,SAAS,IAAI;AAAA,IACvB;AAAA,IACA,YAAY,QAAQ;AAAA,IACpB,SAAS,QAAQ;AAAA,EACnB;AACF;AAEA,MAAM,uBAAuB;AAAA,EAA7B;AACE,SAAQ,aAAa,oBAAI,IAAY;AACrC,SAAQ,eAAyB,CAAC;AAClC,SAAQ,YAAY,oBAAI,IAAwC;AAChE,SAAQ,gBAAgB;AACxB,SAAQ,oBAAoB,oBAAI,IAAoB;AAAA;AAAA,EAEpD,SAAS,eAAkC,SAAyB;AAClE,UAAM,UAAU,8BAA8B;AAC9C,QAAI,cAAc,WAAW,KAAK,KAAK,UAAU,SAAS,EAAG;AAC7D,UAAM,UAAU,oBAAoB,OAAO;AAC3C,UAAM,gBAAgB,QACnB,MAAM,EACN,KAAK,CAAC,GAAG,OAAO,EAAE,QAAQ,YAAY,qBAAqB,EAAE,QAAQ,YAAY,iBAAiB;AAErG,eAAW,gBAAgB,eAAe;AACxC,UAAI,KAAK,WAAW,IAAI,aAAa,EAAE,EAAG;AAE1C,UAAI,aAAa;AACjB,UAAI,2BAA2B;AAC/B,YAAM,YAAY,yBAAyB,cAAc,QAAQ,CAAC;AAElE,iBAAW,SAAS,eAAe;AACjC,cAAM,UAAU,MAAM;AACtB,YAAI,CAAC,YAAY,QAAQ,kBAAkB,aAAa,IAAI,EAAG;AAC/D,YAAI,CAAC,gBAAgB,QAAQ,UAAU,QAAQ,QAAQ,GAAG;AACxD,qCAA2B;AAC3B;AAAA,QACF;AACA,YAAI,KAAK,eAAe,SAAS,aAAa,EAAE,EAAG;AACnD,qBAAa;AACb,YAAI;AACF,eAAK,QAAQ,QAAQ,QAAQ,OAAO,WAAW,OAAO,CAAC;AAAA,QACzD,SAAS,OAAO;AACd,iBAAO,MAAM,+BAA+B,EAAE,WAAW,QAAQ,IAAI,KAAK,MAAM,CAAC;AAAA,QACnF;AAAA,MACF;AAEA,iBAAW,YAAY,KAAK,UAAU,OAAO,GAAG;AAC9C,YAAI,CAAC,YAAY,SAAS,SAAS,aAAa,IAAI,EAAG;AACvD,qBAAa;AACb,YAAI;AACF,mBAAS,OAAO,YAAY;AAAA,QAC9B,SAAS,OAAO;AACd,iBAAO,MAAM,uCAAuC,EAAE,KAAK,MAAM,CAAC;AAAA,QACpE;AAAA,MACF;AAOA,UAAI,cAAc,CAAC,0BAA0B;AAC3C,aAAK,YAAY,aAAa,EAAE;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,SAA4B,QAA6D;AACjG,UAAM,KAAK,KAAK;AAChB,SAAK,UAAU,IAAI,IAAI,EAAE,IAAI,SAAS,OAAO,CAAC;AAC9C,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,EAAE;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,gBAAgB;AACd,SAAK,WAAW,MAAM;AACtB,SAAK,eAAe,CAAC;AACrB,SAAK,UAAU,MAAM;AACrB,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,MAAM;AAAA,EAC/B;AAAA,EAEQ,eAAe,SAA8B,gBAAiC;AACpF,QAAI,CAAC,QAAQ,cAAc,QAAQ,cAAc,EAAG,QAAO;AAC3D,UAAM,MAAM,GAAG,QAAQ,EAAE,IAAI,cAAc;AAC3C,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAAW,KAAK,kBAAkB,IAAI,GAAG;AAC/C,QAAI,OAAO,aAAa,YAAY,MAAM,WAAW,QAAQ,YAAY;AACvE,aAAO;AAAA,IACT;AACA,SAAK,kBAAkB,IAAI,KAAK,GAAG;AACnC,WAAO;AAAA,EACT;AAAA,EAEQ,YAAY,gBAAwB;AAC1C,SAAK,WAAW,IAAI,cAAc;AAClC,SAAK,aAAa,KAAK,cAAc;AACrC,QAAI,KAAK,aAAa,UAAU,gBAAiB;AACjD,UAAM,cAAc,KAAK,aAAa,SAAS;AAC/C,UAAM,UAAU,KAAK,aAAa,OAAO,GAAG,WAAW;AACvD,eAAW,MAAM,SAAS;AACxB,WAAK,WAAW,OAAO,EAAE;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,MAAM,aAAa,IAAI,uBAAuB;AAEvC,SAAS,6BACd,eACA,SACA;AACA,aAAW,SAAS,eAAe,OAAO;AAC5C;AAEO,SAAS,6BACd,SACA,QACY;AACZ,SAAO,WAAW,UAAU,SAAS,MAAM;AAC7C;AAEO,SAAS,yCAAmD;AACjE,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,UAAU,8BAA8B;AAC9C,aAAW,SAAS,SAAS;AAC3B,eAAW,WAAW,MAAM,QAAQ,YAAY,CAAC,GAAG;AAClD,UAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,iBAAS,IAAI,OAAO;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,QAAQ;AAC5B;AAEO,SAAS,wCAAwC;AACtD,aAAW,cAAc;AAC3B;",
4
+ "sourcesContent": ["import { flash, type FlashKind } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n getNotificationHandlerEntries,\n} from '@open-mercato/shared/lib/notifications/handler-registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type {\n NotificationHandler,\n NotificationHandlerContext,\n NotificationHandlerPopupOptions,\n NotificationHandlerToastOptions,\n} from '@open-mercato/shared/modules/notifications/handler'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { localizeNotificationCopy } from './localizeNotification'\n\nconst logger = createLogger('ui').child({ component: 'notifications' })\n\ntype RuntimeContext = {\n userId?: string\n features: string[]\n currentPath: string\n t?: (key: string, fallback?: string) => string\n refreshNotifications: () => void\n navigate: (href: string) => void\n markAsRead: (notificationId: string) => Promise<void>\n dismiss: (notificationId: string) => Promise<void>\n}\n\ntype NotificationEffectListener = {\n id: number\n pattern: string | string[]\n effect: (notification: NotificationDto) => void\n}\n\nconst DEFAULT_PRIORITY = 50\nconst MAX_TRACKED_IDS = 500\nconst RETAINED_IDS_AFTER_PRUNE = 200\nconst POPUP_EVENT_NAME = 'om:notifications:popup'\n\nfunction toFlashKind(value: NotificationHandlerToastOptions['severity'] | NotificationHandlerPopupOptions['severity']): FlashKind {\n if (value === 'success') return 'success'\n if (value === 'warning') return 'warning'\n if (value === 'error') return 'error'\n return 'info'\n}\n\nfunction matchesType(pattern: string | string[], type: string): boolean {\n const patterns = Array.isArray(pattern) ? pattern : [pattern]\n return patterns.some((current) => {\n if (current === '*') return true\n if (current.endsWith('.*')) return type.startsWith(current.slice(0, -1))\n return current === type\n })\n}\n\nfunction matchesFeatures(required: string[] | undefined, current: string[]): boolean {\n return hasAllFeatures(current, required)\n}\n\nfunction buildHandlerContext(runtime: RuntimeContext): NotificationHandlerContext {\n return {\n userId: runtime.userId,\n features: runtime.features,\n currentPath: runtime.currentPath,\n t: runtime.t,\n toast(options) {\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n popup(options) {\n if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {\n window.dispatchEvent(new CustomEvent(POPUP_EVENT_NAME, { detail: options }))\n }\n const text = options.body ? `${options.title}: ${options.body}` : options.title\n flash(text, toFlashKind(options.severity))\n },\n emitEvent(eventName, detail) {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(eventName, { detail }))\n },\n refreshNotifications: runtime.refreshNotifications,\n navigate(href) {\n runtime.navigate(href)\n },\n markAsRead: runtime.markAsRead,\n dismiss: runtime.dismiss,\n }\n}\n\nclass NotificationDispatcher {\n private handledIds = new Set<string>()\n private handledOrder: string[] = []\n private listeners = new Map<number, NotificationEffectListener>()\n private listenerIdSeq = 1\n private debounceByHandler = new Map<string, number>()\n\n dispatch(notifications: NotificationDto[], runtime: RuntimeContext) {\n const entries = getNotificationHandlerEntries()\n if (notifications.length === 0 && this.listeners.size === 0) return\n const context = buildHandlerContext(runtime)\n const sortedEntries = entries\n .slice()\n .sort((a, b) => (b.handler.priority ?? DEFAULT_PRIORITY) - (a.handler.priority ?? DEFAULT_PRIORITY))\n\n for (const notification of notifications) {\n if (this.handledIds.has(notification.id)) continue\n\n let handlerRan = false\n let hadFeatureBlockedHandler = false\n const localized = localizeNotificationCopy(notification, runtime.t)\n\n for (const entry of sortedEntries) {\n const handler = entry.handler\n if (!matchesType(handler.notificationType, notification.type)) continue\n if (!matchesFeatures(handler.features, runtime.features)) {\n hadFeatureBlockedHandler = true\n continue\n }\n if (this.shouldDebounce(handler, notification.id)) continue\n handlerRan = true\n try {\n void Promise.resolve(handler.handle(localized, context))\n } catch (error) {\n logger.error('Notification handler failed', { handlerId: handler.id, err: error })\n }\n }\n\n for (const listener of this.listeners.values()) {\n if (!matchesType(listener.pattern, notification.type)) continue\n handlerRan = true\n try {\n listener.effect(notification)\n } catch (error) {\n logger.error('Notification effect listener failed', { err: error })\n }\n }\n\n // Only mark as permanently handled when something actually ran, or when no handler\n // was blocked by missing features. This prevents a race where the initial notification\n // fetch completes before the feature-check API resolves (features: []), causing all\n // feature-gated handlers to be skipped \u2014 but the notification gets permanently\n // deduped so it never fires when features are available on the next dispatch.\n if (handlerRan || !hadFeatureBlockedHandler) {\n this.markHandled(notification.id)\n }\n }\n }\n\n subscribe(pattern: string | string[], effect: (notification: NotificationDto) => void): () => void {\n const id = this.listenerIdSeq++\n this.listeners.set(id, { id, pattern, effect })\n return () => {\n this.listeners.delete(id)\n }\n }\n\n resetForTests() {\n this.handledIds.clear()\n this.handledOrder = []\n this.listeners.clear()\n this.listenerIdSeq = 1\n this.debounceByHandler.clear()\n }\n\n private shouldDebounce(handler: NotificationHandler, notificationId: string): boolean {\n if (!handler.debounceMs || handler.debounceMs <= 0) return false\n const key = `${handler.id}:${notificationId}`\n const now = Date.now()\n const previous = this.debounceByHandler.get(key)\n if (typeof previous === 'number' && now - previous < handler.debounceMs) {\n return true\n }\n this.debounceByHandler.set(key, now)\n return false\n }\n\n private markHandled(notificationId: string) {\n this.handledIds.add(notificationId)\n this.handledOrder.push(notificationId)\n if (this.handledOrder.length <= MAX_TRACKED_IDS) return\n const removeCount = this.handledOrder.length - RETAINED_IDS_AFTER_PRUNE\n const removed = this.handledOrder.splice(0, removeCount)\n for (const id of removed) {\n this.handledIds.delete(id)\n }\n }\n}\n\nconst dispatcher = new NotificationDispatcher()\n\nexport function dispatchNotificationHandlers(\n notifications: NotificationDto[],\n runtime: RuntimeContext,\n) {\n dispatcher.dispatch(notifications, runtime)\n}\n\nexport function subscribeNotificationEffects(\n pattern: string | string[],\n effect: (notification: NotificationDto) => void,\n): () => void {\n return dispatcher.subscribe(pattern, effect)\n}\n\nexport function getRequiredNotificationHandlerFeatures(): string[] {\n const required = new Set<string>()\n const entries = getNotificationHandlerEntries()\n for (const entry of entries) {\n for (const feature of entry.handler.features ?? []) {\n if (feature && feature.trim().length > 0) {\n required.add(feature)\n }\n }\n }\n return Array.from(required)\n}\n\nexport function __resetNotificationDispatcherForTests() {\n dispatcher.resetForTests()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,9 +3,6 @@ import * as React from "react";
3
3
  import { apiCall, apiCallOrThrow } from "../utils/apiCall.js";
4
4
  import { useGuardedMutation } from "../injection/useGuardedMutation.js";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
- import {
7
- NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE
8
- } from "@open-mercato/shared/modules/notifications/types";
9
6
  const NOTIFICATION_ACTIONS_CONTEXT_ID = "notifications-actions";
10
7
  function useNotificationActions(notifications, setNotifications, setUnreadCount) {
11
8
  const markAsReadRef = React.useRef(async () => {
@@ -55,17 +52,9 @@ function useNotificationActions(notifications, setNotifications, setUnreadCount)
55
52
  )
56
53
  );
57
54
  setUnreadCount((prev) => Math.max(0, prev - 1));
58
- } else if (result.status === 409 && result.result?.code === NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE) {
59
- const notification = notifications.find((n) => n.id === id);
60
- setNotifications(
61
- (prev) => prev.map((n) => n.id === id ? { ...n, status: "actioned" } : n)
62
- );
63
- if (notification?.status === "unread") {
64
- setUnreadCount((prev) => Math.max(0, prev - 1));
65
- }
66
55
  }
67
56
  return { href: result.result?.href };
68
- }, [notifications, runMutation, retryLastMutation, setNotifications, setUnreadCount]);
57
+ }, [runMutation, retryLastMutation, setNotifications, setUnreadCount]);
69
58
  const dismiss = React.useCallback(
70
59
  async (id) => {
71
60
  await runMutation({
@@ -109,7 +98,7 @@ function useNotificationActions(notifications, setNotifications, setUnreadCount)
109
98
  const next = [
110
99
  {
111
100
  ...dismissUndo.notification,
112
- status: dismissUndo.notification.status === "actioned" ? "actioned" : dismissUndo.previousStatus,
101
+ status: dismissUndo.previousStatus,
113
102
  readAt: dismissUndo.previousStatus === "unread" ? null : dismissUndo.notification.readAt ?? (/* @__PURE__ */ new Date()).toISOString()
114
103
  },
115
104
  ...prev.filter((n) => n.id !== dismissUndo.notification.id)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/notifications/useNotificationActions.ts"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall, apiCallOrThrow } from '../utils/apiCall'\nimport { useGuardedMutation } from '../injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n type NotificationDto,\n} from '@open-mercato/shared/modules/notifications/types'\n\nconst NOTIFICATION_ACTIONS_CONTEXT_ID = 'notifications-actions'\n\ntype NotificationMutationContext = {\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n}\n\nexport type NotificationDismissUndoState = {\n notification: NotificationDto\n previousStatus: 'read' | 'unread'\n} | null\n\ntype SetUnreadCount = React.Dispatch<React.SetStateAction<number>>\n\nexport type NotificationActionsResult = {\n markAsRead: (id: string) => Promise<void>\n executeAction: (id: string, actionId: string) => Promise<{ href?: string }>\n dismiss: (id: string) => Promise<void>\n dismissUndo: NotificationDismissUndoState\n undoDismiss: () => Promise<void>\n markAllRead: () => Promise<void>\n markAsReadRef: React.MutableRefObject<(id: string) => Promise<void>>\n dismissRef: React.MutableRefObject<(id: string) => Promise<void>>\n}\n\nexport function useNotificationActions(\n notifications: NotificationDto[],\n setNotifications: React.Dispatch<React.SetStateAction<NotificationDto[]>>,\n setUnreadCount: SetUnreadCount,\n): NotificationActionsResult {\n const markAsReadRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const dismissRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const [dismissUndo, setDismissUndo] = React.useState<NotificationDismissUndoState>(null)\n const dismissUndoTimerRef = React.useRef<number | null>(null)\n\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<NotificationMutationContext>({\n contextId: NOTIFICATION_ACTIONS_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const markAsRead = React.useCallback(async (id: string) => {\n await runMutation({\n operation: () => apiCallOrThrow(`/api/notifications/${id}/read`, { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id },\n })\n setNotifications((prev) =>\n prev.map((n) =>\n n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n,\n ),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n React.useEffect(() => {\n markAsReadRef.current = markAsRead\n }, [markAsRead])\n\n const executeAction = React.useCallback(async (id: string, actionId: string) => {\n const result = await runMutation({\n operation: () =>\n apiCall<{ ok?: boolean; href?: string; code?: string }>(\n `/api/notifications/${id}/action`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ actionId }),\n },\n ),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id, actionId },\n })\n\n if (result.ok) {\n setNotifications((prev) =>\n prev.map((n) =>\n n.id === id ? { ...n, status: 'actioned', actionTaken: actionId } : n,\n ),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n } else if (\n result.status === 409 &&\n result.result?.code === NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE\n ) {\n const notification = notifications.find((n) => n.id === id)\n setNotifications((prev) =>\n prev.map((n) => (n.id === id ? { ...n, status: 'actioned' } : n)),\n )\n if (notification?.status === 'unread') {\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }\n }\n\n return { href: result.result?.href }\n }, [notifications, runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n const dismiss = React.useCallback(\n async (id: string) => {\n await runMutation({\n operation: () => apiCallOrThrow(`/api/notifications/${id}/dismiss`, { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id },\n })\n const notification = notifications.find((n) => n.id === id)\n setNotifications((prev) => prev.filter((n) => n.id !== id))\n if (notification?.status === 'unread') {\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }\n if (notification) {\n const previousStatus = notification.status === 'unread' ? 'unread' : 'read'\n setDismissUndo({ notification, previousStatus })\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n dismissUndoTimerRef.current = window.setTimeout(() => {\n setDismissUndo(null)\n }, 6000)\n }\n },\n [notifications, setNotifications, setUnreadCount, runMutation, retryLastMutation],\n )\n\n React.useEffect(() => {\n dismissRef.current = dismiss\n }, [dismiss])\n\n const undoDismiss = React.useCallback(async () => {\n if (!dismissUndo) return\n await runMutation({\n operation: () =>\n apiCallOrThrow(`/api/notifications/${dismissUndo.notification.id}/restore`, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ status: dismissUndo.previousStatus }),\n }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id: dismissUndo.notification.id, status: dismissUndo.previousStatus },\n })\n\n setNotifications((prev) => {\n const next = [\n {\n ...dismissUndo.notification,\n status:\n dismissUndo.notification.status === 'actioned' ? 'actioned' : dismissUndo.previousStatus,\n readAt:\n dismissUndo.previousStatus === 'unread'\n ? null\n : dismissUndo.notification.readAt ?? new Date().toISOString(),\n },\n ...prev.filter((n) => n.id !== dismissUndo.notification.id),\n ]\n return next.sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),\n )\n })\n\n if (dismissUndo.previousStatus === 'unread') {\n setUnreadCount((prev) => prev + 1)\n }\n\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n setDismissUndo(null)\n }, [dismissUndo, setNotifications, setUnreadCount, runMutation, retryLastMutation])\n\n const markAllRead = React.useCallback(async () => {\n await runMutation({\n operation: () => apiCallOrThrow('/api/notifications/mark-all-read', { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: {},\n })\n setNotifications((prev) =>\n prev.map((n) =>\n n.status === 'unread'\n ? { ...n, status: 'read', readAt: new Date().toISOString() }\n : n,\n ),\n )\n setUnreadCount(0)\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n React.useEffect(() => {\n return () => {\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n }\n }, [])\n\n return {\n markAsRead,\n executeAction,\n dismiss,\n dismissUndo,\n undoDismiss,\n markAllRead,\n markAsReadRef,\n dismissRef,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall, apiCallOrThrow } from '../utils/apiCall'\nimport { useGuardedMutation } from '../injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\n\nconst NOTIFICATION_ACTIONS_CONTEXT_ID = 'notifications-actions'\n\ntype NotificationMutationContext = {\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n}\n\nexport type NotificationDismissUndoState = {\n notification: NotificationDto\n previousStatus: 'read' | 'unread'\n} | null\n\ntype SetUnreadCount = React.Dispatch<React.SetStateAction<number>>\n\nexport type NotificationActionsResult = {\n markAsRead: (id: string) => Promise<void>\n executeAction: (id: string, actionId: string) => Promise<{ href?: string }>\n dismiss: (id: string) => Promise<void>\n dismissUndo: NotificationDismissUndoState\n undoDismiss: () => Promise<void>\n markAllRead: () => Promise<void>\n markAsReadRef: React.MutableRefObject<(id: string) => Promise<void>>\n dismissRef: React.MutableRefObject<(id: string) => Promise<void>>\n}\n\nexport function useNotificationActions(\n notifications: NotificationDto[],\n setNotifications: React.Dispatch<React.SetStateAction<NotificationDto[]>>,\n setUnreadCount: SetUnreadCount,\n): NotificationActionsResult {\n const markAsReadRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const dismissRef = React.useRef<(id: string) => Promise<void>>(async () => {})\n const [dismissUndo, setDismissUndo] = React.useState<NotificationDismissUndoState>(null)\n const dismissUndoTimerRef = React.useRef<number | null>(null)\n\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<NotificationMutationContext>({\n contextId: NOTIFICATION_ACTIONS_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const markAsRead = React.useCallback(async (id: string) => {\n await runMutation({\n operation: () => apiCallOrThrow(`/api/notifications/${id}/read`, { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id },\n })\n setNotifications((prev) =>\n prev.map((n) =>\n n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n,\n ),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n React.useEffect(() => {\n markAsReadRef.current = markAsRead\n }, [markAsRead])\n\n const executeAction = React.useCallback(async (id: string, actionId: string) => {\n const result = await runMutation({\n operation: () =>\n apiCall<{ ok: boolean; href?: string }>(\n `/api/notifications/${id}/action`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ actionId }),\n },\n ),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id, actionId },\n })\n\n if (result.ok) {\n setNotifications((prev) =>\n prev.map((n) =>\n n.id === id ? { ...n, status: 'actioned', actionTaken: actionId } : n,\n ),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }\n\n return { href: result.result?.href }\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n const dismiss = React.useCallback(\n async (id: string) => {\n await runMutation({\n operation: () => apiCallOrThrow(`/api/notifications/${id}/dismiss`, { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id },\n })\n const notification = notifications.find((n) => n.id === id)\n setNotifications((prev) => prev.filter((n) => n.id !== id))\n if (notification?.status === 'unread') {\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }\n if (notification) {\n const previousStatus = notification.status === 'unread' ? 'unread' : 'read'\n setDismissUndo({ notification, previousStatus })\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n dismissUndoTimerRef.current = window.setTimeout(() => {\n setDismissUndo(null)\n }, 6000)\n }\n },\n [notifications, setNotifications, setUnreadCount, runMutation, retryLastMutation],\n )\n\n React.useEffect(() => {\n dismissRef.current = dismiss\n }, [dismiss])\n\n const undoDismiss = React.useCallback(async () => {\n if (!dismissUndo) return\n await runMutation({\n operation: () =>\n apiCallOrThrow(`/api/notifications/${dismissUndo.notification.id}/restore`, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ status: dismissUndo.previousStatus }),\n }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: { id: dismissUndo.notification.id, status: dismissUndo.previousStatus },\n })\n\n setNotifications((prev) => {\n const next = [\n {\n ...dismissUndo.notification,\n status: dismissUndo.previousStatus,\n readAt:\n dismissUndo.previousStatus === 'unread'\n ? null\n : dismissUndo.notification.readAt ?? new Date().toISOString(),\n },\n ...prev.filter((n) => n.id !== dismissUndo.notification.id),\n ]\n return next.sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),\n )\n })\n\n if (dismissUndo.previousStatus === 'unread') {\n setUnreadCount((prev) => prev + 1)\n }\n\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n setDismissUndo(null)\n }, [dismissUndo, setNotifications, setUnreadCount, runMutation, retryLastMutation])\n\n const markAllRead = React.useCallback(async () => {\n await runMutation({\n operation: () => apiCallOrThrow('/api/notifications/mark-all-read', { method: 'PUT' }),\n context: { formId: NOTIFICATION_ACTIONS_CONTEXT_ID, resourceKind: 'notification', retryLastMutation },\n mutationPayload: {},\n })\n setNotifications((prev) =>\n prev.map((n) =>\n n.status === 'unread'\n ? { ...n, status: 'read', readAt: new Date().toISOString() }\n : n,\n ),\n )\n setUnreadCount(0)\n }, [runMutation, retryLastMutation, setNotifications, setUnreadCount])\n\n React.useEffect(() => {\n return () => {\n if (dismissUndoTimerRef.current) {\n window.clearTimeout(dismissUndoTimerRef.current)\n }\n }\n }, [])\n\n return {\n markAsRead,\n executeAction,\n dismiss,\n dismissUndo,\n undoDismiss,\n markAllRead,\n markAsReadRef,\n dismissRef,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,19 +6,7 @@ import { pushOperation } from "../operations/store.js";
6
6
  import { pushPartialIndexWarning } from "../indexes/store.js";
7
7
  import { createScopedHeaderStack } from "./scopedHeaderStack.js";
8
8
  import { createLogger } from "@open-mercato/shared/lib/logger";
9
- import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
10
9
  const logger = createLogger("ui").child({ component: "apiFetch" });
11
- const fallbackAuthNoticeTranslator = (key) => key;
12
- let authNoticeTranslator = null;
13
- function registerAuthNoticeTranslator(translate) {
14
- authNoticeTranslator = translate;
15
- return () => {
16
- if (authNoticeTranslator === translate) authNoticeTranslator = null;
17
- };
18
- }
19
- function translateAuthNotice(key, fallback, params) {
20
- return translateWithFallback(authNoticeTranslator ?? fallbackAuthNoticeTranslator, key, fallback, params);
21
- }
22
10
  const scopedHeaders = createScopedHeaderStack();
23
11
  function mergeHeaders(base, scoped) {
24
12
  const headers = new Headers(base ?? {});
@@ -92,7 +80,7 @@ function redirectToSessionRefresh() {
92
80
  if (recentlyAttemptedSessionRefresh(current)) return;
93
81
  recordSessionRefreshAttempt(current);
94
82
  try {
95
- flash(translateAuthNotice("ui.auth.sessionExpired", "Session expired. Redirecting to sign in\u2026"), "warning");
83
+ flash("Session expired. Redirecting to sign in\u2026", "warning");
96
84
  setTimeout(() => {
97
85
  window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`;
98
86
  }, 20);
@@ -119,20 +107,12 @@ function formatForbiddenAccessMessage(options) {
119
107
  const roles = options?.requiredRoles?.filter(Boolean) ?? [];
120
108
  const effectiveRoles = roles.length ? roles : DEFAULT_FORBIDDEN_ROLES.filter(Boolean);
121
109
  if (features.length) {
122
- return translateAuthNotice(
123
- "ui.auth.accessDenied.missingFeatures",
124
- 'Access denied: you are missing the required permission "{features}". Contact your administrator.',
125
- { features: features.join(", ") }
126
- );
110
+ return `Access denied: you are missing the required permission "${features.join(", ")}". Contact your administrator.`;
127
111
  }
128
112
  if (effectiveRoles.length) {
129
- return translateAuthNotice(
130
- "ui.auth.accessDenied.requiredRoles",
131
- 'Access denied: this area requires the role "{roles}". Contact your administrator.',
132
- { roles: effectiveRoles.join(", ") }
133
- );
113
+ return `Access denied: this area requires the role "${effectiveRoles.join(", ")}". Contact your administrator.`;
134
114
  }
135
- return translateAuthNotice("ui.auth.accessDenied.generic", "Access denied: you do not have permission to perform this action.");
115
+ return "Access denied: you do not have permission to perform this action.";
136
116
  }
137
117
  function notifyForbiddenAccess(options) {
138
118
  if (typeof window === "undefined") return;
@@ -246,7 +226,6 @@ export {
246
226
  notifyForbiddenAccess,
247
227
  redirectToForbiddenLogin,
248
228
  redirectToSessionRefresh,
249
- registerAuthNoticeTranslator,
250
229
  setAuthRedirectConfig,
251
230
  withScopedApiHeaders
252
231
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/utils/api.ts"],
4
- "sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { TranslateFn, TranslateParams } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\n\nconst logger = createLogger('ui').child({ component: 'apiFetch' })\n\nconst fallbackAuthNoticeTranslator: TranslateFn = (key) => key\nlet authNoticeTranslator: TranslateFn | null = null\n\n/**\n * Register the active i18n translator for the session-expired and\n * access-denied flash banners, which fire outside React. Returns a cleanup\n * that unregisters it unless another translator replaced it meanwhile.\n */\nexport function registerAuthNoticeTranslator(translate: TranslateFn): () => void {\n authNoticeTranslator = translate\n return () => {\n if (authNoticeTranslator === translate) authNoticeTranslator = null\n }\n}\n\nfunction translateAuthNotice(key: string, fallback: string, params?: TranslateParams): string {\n return translateWithFallback(authNoticeTranslator ?? fallbackAuthNoticeTranslator, key, fallback, params)\n}\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nfunction readPathname(): string {\n return typeof window !== 'undefined' ? window.location?.pathname ?? '' : ''\n}\n\n// `/start` is the anonymous landing/dashboard page and, like `/login`, runs a\n// background auth-probe whose 401 is expected for a logged-out visitor \u2014 not a\n// real session problem worth surfacing (see GH #6159).\nfunction isLoginPathname(pathname: string): boolean {\n return pathname.startsWith('/login') || pathname.startsWith('/start')\n}\n\nfunction isPortalPathname(pathname: string): boolean {\n return /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nconst SESSION_REFRESH_ATTEMPT_KEY_PREFIX = 'om:session-refresh-attempt:'\nconst SESSION_REFRESH_COOLDOWN_MS = 10_000\n\n// A genuinely expired session redirects once per target, then goes quiet: the\n// refresh always succeeds (the session cookie is fine), bounces back to the\n// same URL, and re-triggers the same 401 for any non-session cause \u2014 without\n// this guard that bounce repeats forever (GH #5186).\nfunction recentlyAttemptedSessionRefresh(target: string): boolean {\n try {\n const raw = window.sessionStorage.getItem(SESSION_REFRESH_ATTEMPT_KEY_PREFIX + target)\n if (!raw) return false\n const attemptedAt = Number(raw)\n return Number.isFinite(attemptedAt) && Date.now() - attemptedAt < SESSION_REFRESH_COOLDOWN_MS\n } catch {\n return false\n }\n}\n\nfunction recordSessionRefreshAttempt(target: string): void {\n try {\n window.sessionStorage.setItem(SESSION_REFRESH_ATTEMPT_KEY_PREFIX + target, String(Date.now()))\n } catch {\n // no-op\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n if (recentlyAttemptedSessionRefresh(current)) return\n recordSessionRefreshAttempt(current)\n try {\n flash(translateAuthNotice('ui.auth.sessionExpired', 'Session expired. Redirecting to sign in\u2026'), 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n readonly requiredFeatures: string[] | null\n readonly requiredRoles: string[] | null\n\n constructor(\n message = 'Forbidden',\n options?: { requiredFeatures?: string[] | null; requiredRoles?: string[] | null },\n ) {\n super(message)\n this.name = 'ForbiddenError'\n this.requiredFeatures = options?.requiredFeatures?.length ? [...options.requiredFeatures] : null\n this.requiredRoles = options?.requiredRoles?.length ? [...options.requiredRoles] : null\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nfunction formatForbiddenAccessMessage(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }): string {\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const effectiveRoles = roles.length ? roles : DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n if (features.length) {\n return translateAuthNotice(\n 'ui.auth.accessDenied.missingFeatures',\n 'Access denied: you are missing the required permission \"{features}\". Contact your administrator.',\n { features: features.join(', ') },\n )\n }\n if (effectiveRoles.length) {\n return translateAuthNotice(\n 'ui.auth.accessDenied.requiredRoles',\n 'Access denied: this area requires the role \"{roles}\". Contact your administrator.',\n { roles: effectiveRoles.join(', ') },\n )\n }\n return translateAuthNotice('ui.auth.accessDenied.generic', 'Access denied: you do not have permission to perform this action.')\n}\n\n/**\n * Signal a forbidden access attempt for an authenticated user via a flash banner.\n *\n * Authenticated 403 responses must never redirect to `/login` \u2014 that creates an\n * infinite loop because the login page detects the active session and bounces\n * the user back to the failing destination (see GH #2070). Pages that need an\n * inline banner should catch `ForbiddenError` and render `AccessDeniedMessage`\n * from `@open-mercato/ui/backend/detail`.\n */\nexport function notifyForbiddenAccess(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n // Portal routes have their own customer auth \u2014 keep the existing no-op contract.\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash(formatForbiddenAccessMessage(options), 'warning')\n } catch {\n // no-op\n }\n}\n\n/**\n * @deprecated Renamed to {@link notifyForbiddenAccess}. The previous name\n * implied a `/login` redirect that no longer happens (see GH #2070). Kept as an\n * exported alias for one minor version so third-party module imports keep\n * building; update imports to `notifyForbiddenAccess`.\n */\nexport const redirectToForbiddenLogin = notifyForbiddenAccess\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n // Snapshot the pathname BEFORE the request is sent. A 401 for a request that\n // started on the login page must stay silent even when the response lands after\n // the post-login client-side navigation to /backend \u2014 otherwise the stale\n // pre-auth 401 raises a bogus \"Session expired\" banner right after signing in.\n const requestPathname = readPathname()\n const res = await baseFetch(input, mergedInit)\n const responsePathname = readPathname()\n const onLoginPage = isLoginPathname(requestPathname) || isLoginPathname(responsePathname)\n const onPortalRoute = isPortalPathname(requestPathname) || isPortalPathname(responsePathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n logger.warn('Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n notifyForbiddenAccess({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n // Attach ACL hints so callers (e.g. flashMutationError) can name the\n // missing permission instead of surfacing a bare \"Forbidden\" toast.\n throw new ForbiddenError(msg, { requiredFeatures: features, requiredRoles: roles })\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'apiFetch' })\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nfunction readPathname(): string {\n return typeof window !== 'undefined' ? window.location?.pathname ?? '' : ''\n}\n\n// `/start` is the anonymous landing/dashboard page and, like `/login`, runs a\n// background auth-probe whose 401 is expected for a logged-out visitor \u2014 not a\n// real session problem worth surfacing (see GH #6159).\nfunction isLoginPathname(pathname: string): boolean {\n return pathname.startsWith('/login') || pathname.startsWith('/start')\n}\n\nfunction isPortalPathname(pathname: string): boolean {\n return /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nconst SESSION_REFRESH_ATTEMPT_KEY_PREFIX = 'om:session-refresh-attempt:'\nconst SESSION_REFRESH_COOLDOWN_MS = 10_000\n\n// A genuinely expired session redirects once per target, then goes quiet: the\n// refresh always succeeds (the session cookie is fine), bounces back to the\n// same URL, and re-triggers the same 401 for any non-session cause \u2014 without\n// this guard that bounce repeats forever (GH #5186).\nfunction recentlyAttemptedSessionRefresh(target: string): boolean {\n try {\n const raw = window.sessionStorage.getItem(SESSION_REFRESH_ATTEMPT_KEY_PREFIX + target)\n if (!raw) return false\n const attemptedAt = Number(raw)\n return Number.isFinite(attemptedAt) && Date.now() - attemptedAt < SESSION_REFRESH_COOLDOWN_MS\n } catch {\n return false\n }\n}\n\nfunction recordSessionRefreshAttempt(target: string): void {\n try {\n window.sessionStorage.setItem(SESSION_REFRESH_ATTEMPT_KEY_PREFIX + target, String(Date.now()))\n } catch {\n // no-op\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n if (recentlyAttemptedSessionRefresh(current)) return\n recordSessionRefreshAttempt(current)\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n readonly requiredFeatures: string[] | null\n readonly requiredRoles: string[] | null\n\n constructor(\n message = 'Forbidden',\n options?: { requiredFeatures?: string[] | null; requiredRoles?: string[] | null },\n ) {\n super(message)\n this.name = 'ForbiddenError'\n this.requiredFeatures = options?.requiredFeatures?.length ? [...options.requiredFeatures] : null\n this.requiredRoles = options?.requiredRoles?.length ? [...options.requiredRoles] : null\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nfunction formatForbiddenAccessMessage(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }): string {\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const effectiveRoles = roles.length ? roles : DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n if (features.length) {\n return `Access denied: you are missing the required permission \"${features.join(', ')}\". Contact your administrator.`\n }\n if (effectiveRoles.length) {\n return `Access denied: this area requires the role \"${effectiveRoles.join(', ')}\". Contact your administrator.`\n }\n return 'Access denied: you do not have permission to perform this action.'\n}\n\n/**\n * Signal a forbidden access attempt for an authenticated user via a flash banner.\n *\n * Authenticated 403 responses must never redirect to `/login` \u2014 that creates an\n * infinite loop because the login page detects the active session and bounces\n * the user back to the failing destination (see GH #2070). Pages that need an\n * inline banner should catch `ForbiddenError` and render `AccessDeniedMessage`\n * from `@open-mercato/ui/backend/detail`.\n */\nexport function notifyForbiddenAccess(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n // Portal routes have their own customer auth \u2014 keep the existing no-op contract.\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash(formatForbiddenAccessMessage(options), 'warning')\n } catch {\n // no-op\n }\n}\n\n/**\n * @deprecated Renamed to {@link notifyForbiddenAccess}. The previous name\n * implied a `/login` redirect that no longer happens (see GH #2070). Kept as an\n * exported alias for one minor version so third-party module imports keep\n * building; update imports to `notifyForbiddenAccess`.\n */\nexport const redirectToForbiddenLogin = notifyForbiddenAccess\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n // Snapshot the pathname BEFORE the request is sent. A 401 for a request that\n // started on the login page must stay silent even when the response lands after\n // the post-login client-side navigation to /backend \u2014 otherwise the stale\n // pre-auth 401 raises a bogus \"Session expired\" banner right after signing in.\n const requestPathname = readPathname()\n const res = await baseFetch(input, mergedInit)\n const responsePathname = readPathname()\n const onLoginPage = isLoginPathname(requestPathname) || isLoginPathname(responsePathname)\n const onPortalRoute = isPortalPathname(requestPathname) || isPortalPathname(responsePathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n logger.warn('Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n notifyForbiddenAccess({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n // Attach ACL hints so callers (e.g. flashMutationError) can name the\n // missing permission instead of surfacing a bare \"Forbidden\" toast.\n throw new ForbiddenError(msg, { requiredFeatures: features, requiredRoles: roles })\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }