@open-mercato/ui 0.5.1-develop.2970.ec144ae954 → 0.5.1-develop.2975.ccbadc8198

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.
@@ -69,6 +69,8 @@ function useMessageCompose({
69
69
  const [submitMode, setSubmitMode] = React.useState("send");
70
70
  const [submitError, setSubmitError] = React.useState(null);
71
71
  const isOpenRef = React.useRef(false);
72
+ const wasOpenRef = React.useRef(false);
73
+ const submitLockReleaseRef = React.useRef(null);
72
74
  const messageTypesQuery = useQuery({
73
75
  queryKey: ["messages", "types"],
74
76
  enabled: variant === "compose" && isOpen,
@@ -167,6 +169,25 @@ function useMessageCompose({
167
169
  normalizedRequiredActionMode,
168
170
  requiredActionConfig?.defaultActionType
169
171
  ]);
172
+ React.useEffect(() => {
173
+ if (!isOpen) {
174
+ submitLockReleaseRef.current?.();
175
+ submitLockReleaseRef.current = null;
176
+ wasOpenRef.current = false;
177
+ return;
178
+ }
179
+ const justOpened = isOpen && !wasOpenRef.current;
180
+ wasOpenRef.current = isOpen;
181
+ if (!justOpened) return;
182
+ submitLockReleaseRef.current?.();
183
+ submitLockReleaseRef.current = null;
184
+ setSubmitting(false);
185
+ setSubmitMode("send");
186
+ }, [isOpen]);
187
+ React.useEffect(() => () => {
188
+ submitLockReleaseRef.current?.();
189
+ submitLockReleaseRef.current = null;
190
+ }, []);
170
191
  React.useEffect(() => {
171
192
  if (!isOpen) return;
172
193
  if (variant !== "forward") return;
@@ -349,6 +370,8 @@ function useMessageCompose({
349
370
  }
350
371
  setSubmitMode(isComposeDraftSubmit ? "draft" : "send");
351
372
  setSubmitting(true);
373
+ let keepSubmitLock = false;
374
+ let shouldReturnFalse = false;
352
375
  try {
353
376
  let nextAttachmentIds = attachmentIds;
354
377
  if (operation.requiresAttachmentRefresh) {
@@ -358,36 +381,50 @@ function useMessageCompose({
358
381
  const message = error instanceof Error ? error.message : t("messages.errors.loadAttachmentOptionsFailed", "Failed to load attachments.");
359
382
  setSubmitError(message);
360
383
  flash(message, "error");
361
- return false;
384
+ shouldReturnFalse = true;
362
385
  }
363
386
  }
364
- const { endpoint, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds });
365
- const call = await apiCall(endpoint, {
366
- method: "POST",
367
- headers: { "Content-Type": "application/json" },
368
- body: JSON.stringify(payload)
369
- });
370
- if (!call.ok) {
371
- const message = toErrorMessage(call.result) ?? t("messages.errors.sendFailed", "Failed to send message.");
372
- setSubmitError(message);
373
- flash(message, "error");
374
- return false;
375
- }
376
- flash(operation.successMessage, "success");
377
- onSuccess?.({ id: call.result?.id });
378
- if (!inline) {
379
- onOpenChange?.(false);
387
+ if (!shouldReturnFalse) {
388
+ const { endpoint, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds });
389
+ const call = await apiCall(endpoint, {
390
+ method: "POST",
391
+ headers: { "Content-Type": "application/json" },
392
+ body: JSON.stringify(payload)
393
+ });
394
+ if (!call.ok) {
395
+ const message = toErrorMessage(call.result) ?? t("messages.errors.sendFailed", "Failed to send message.");
396
+ setSubmitError(message);
397
+ flash(message, "error");
398
+ shouldReturnFalse = true;
399
+ } else {
400
+ flash(operation.successMessage, "success");
401
+ keepSubmitLock = true;
402
+ onSuccess?.({ id: call.result?.id });
403
+ if (!inline) {
404
+ onOpenChange?.(false);
405
+ }
406
+ }
380
407
  }
381
- return true;
382
408
  } catch (error) {
383
409
  const message = error instanceof Error ? error.message : t("messages.errors.sendFailed", "Failed to send message.");
384
410
  setSubmitError(message);
385
411
  flash(message, "error");
386
- return false;
412
+ shouldReturnFalse = true;
387
413
  } finally {
388
- setSubmitting(false);
414
+ if (!keepSubmitLock) {
415
+ setSubmitting(false);
416
+ }
389
417
  setSubmitMode("send");
390
418
  }
419
+ if (shouldReturnFalse) {
420
+ return false;
421
+ }
422
+ if (keepSubmitLock) {
423
+ return await new Promise((resolve) => {
424
+ submitLockReleaseRef.current = () => resolve(true);
425
+ });
426
+ }
427
+ return true;
391
428
  }, [
392
429
  attachmentIds,
393
430
  composeDraftOperation,
@@ -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 } from '../utils/apiCall'\nimport type {\n AttachmentListResponse,\n MessageComposerProps,\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} 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\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 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 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 defaultValues,\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 // 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\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) 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 // Recipient lookup is filtered in TagsInput because incremental auth user search is unreliable.\n\n const call = await apiCall<{ items?: UserListItem[] }>(`/api/auth/users?${params.toString()}`)\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 const composeSendOperation = useComposeSendOperation({\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 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 handleSubmit = React.useCallback(async ({ saveAsDraft = false }: { saveAsDraft?: boolean } = {}) => {\n if (submitting) return false\n\n setSubmitError(null)\n\n const isComposeDraftSubmit = saveAsDraft && variant === 'compose'\n const operation = isComposeDraftSubmit\n ? composeDraftOperation\n : variant === 'compose'\n ? 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\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 return false\n }\n }\n\n const { endpoint, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })\n\n const call = await apiCall<{ id?: string }>(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n\n if (!call.ok) {\n const message = toErrorMessage(call.result) ?? t('messages.errors.sendFailed', 'Failed to send message.')\n setSubmitError(message)\n flash(message, 'error')\n return false\n }\n\n flash(operation.successMessage, 'success')\n\n onSuccess?.({ id: call.result?.id })\n\n if (!inline) {\n onOpenChange?.(false)\n }\n return true\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 return false\n } finally {\n setSubmitting(false)\n setSubmitMode('send')\n }\n }, [\n attachmentIds,\n composeDraftOperation,\n composeSendOperation,\n forwardOperation,\n inline,\n loadAttachmentIds,\n onOpenChange,\n onSuccess,\n replyOperation,\n submitting,\n t,\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 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 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 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 } from '../utils/apiCall'\nimport type {\n AttachmentListResponse,\n MessageComposerProps,\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} 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\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 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 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 defaultValues,\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 // 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 // Recipient lookup is filtered in TagsInput because incremental auth user search is unreliable.\n\n const call = await apiCall<{ items?: UserListItem[] }>(`/api/auth/users?${params.toString()}`)\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 const composeSendOperation = useComposeSendOperation({\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 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 handleSubmit = React.useCallback(async ({ saveAsDraft = false }: { saveAsDraft?: boolean } = {}) => {\n if (submitting) return false\n\n setSubmitError(null)\n\n const isComposeDraftSubmit = saveAsDraft && variant === 'compose'\n const operation = isComposeDraftSubmit\n ? composeDraftOperation\n : variant === 'compose'\n ? 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, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })\n\n const call = await apiCall<{ id?: string }>(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n\n if (!call.ok) {\n const message = toErrorMessage(call.result) ?? t('messages.errors.sendFailed', 'Failed to send message.')\n setSubmitError(message)\n flash(message, 'error')\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 forwardOperation,\n inline,\n loadAttachmentIds,\n onOpenChange,\n onSuccess,\n replyOperation,\n submitting,\n t,\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 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 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 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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",
6
6
  "names": ["defaultType"]
7
7
  }
package/jest.setup.ts CHANGED
@@ -22,6 +22,12 @@ class MockResponse {
22
22
  async text() {
23
23
  return this.body
24
24
  }
25
+
26
+ clone() {
27
+ const cloned = new MockResponse(this.body, { status: this.status })
28
+ cloned.headers = new Map(this.headers)
29
+ return cloned
30
+ }
25
31
  }
26
32
 
27
33
  if (typeof globalThis.Response === 'undefined') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.5.1-develop.2970.ec144ae954",
3
+ "version": "0.5.1-develop.2975.ccbadc8198",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -134,12 +134,12 @@
134
134
  "recharts": "^3.8.1"
135
135
  },
136
136
  "peerDependencies": {
137
- "@open-mercato/shared": "0.5.1-develop.2970.ec144ae954",
137
+ "@open-mercato/shared": "0.5.1-develop.2975.ccbadc8198",
138
138
  "react": ">=18.0.0",
139
139
  "react-dom": ">=18.0.0"
140
140
  },
141
141
  "devDependencies": {
142
- "@open-mercato/shared": "0.5.1-develop.2970.ec144ae954",
142
+ "@open-mercato/shared": "0.5.1-develop.2975.ccbadc8198",
143
143
  "@testing-library/dom": "^10.4.1",
144
144
  "@testing-library/jest-dom": "^6.9.1",
145
145
  "@testing-library/react": "^16.3.1",
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import * as React from 'react'
6
- import { fireEvent, screen, waitFor, within } from '@testing-library/react'
6
+ import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
7
7
  import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
8
8
  import { MessageComposer } from '../MessageComposer'
9
9
  import { apiCall } from '../../utils/apiCall'
@@ -121,6 +121,107 @@ describe('MessageComposer draft flow', () => {
121
121
  expect(flash).toHaveBeenCalledWith('Draft saved.', 'success')
122
122
  })
123
123
 
124
+ it('keeps the send action disabled after a successful compose submit until the composer resets', async () => {
125
+ let resolveMessagePost!: (value: {
126
+ ok: boolean
127
+ status: number
128
+ result: { id: string }
129
+ response: { status: number }
130
+ }) => void
131
+
132
+ ;(apiCall as jest.Mock).mockImplementation((url: string, options?: { method?: string, body?: string }) => {
133
+ if (url.startsWith('/api/messages/types')) {
134
+ return Promise.resolve({
135
+ ok: true,
136
+ status: 200,
137
+ result: {
138
+ items: [{
139
+ type: 'default',
140
+ module: 'messages',
141
+ labelKey: 'messages.types.default',
142
+ icon: 'mail',
143
+ allowReply: true,
144
+ allowForward: true,
145
+ }],
146
+ },
147
+ response: { status: 200 },
148
+ })
149
+ }
150
+
151
+ if (url === '/api/messages' && options?.method === 'POST') {
152
+ return new Promise((resolve) => {
153
+ resolveMessagePost = resolve
154
+ })
155
+ }
156
+
157
+ return Promise.resolve({
158
+ ok: true,
159
+ status: 200,
160
+ result: { items: [] },
161
+ response: { status: 200 },
162
+ })
163
+ })
164
+
165
+ renderWithProviders(
166
+ <MessageComposer
167
+ inline
168
+ variant="compose"
169
+ defaultValues={{
170
+ recipients: ['11111111-1111-4111-8111-111111111111'],
171
+ subject: 'Send lock test',
172
+ body: 'Send lock body',
173
+ }}
174
+ />,
175
+ { dict: {} },
176
+ )
177
+
178
+ await waitFor(() => {
179
+ expect((apiCall as jest.Mock).mock.calls.some((call) => call[0] === '/api/messages/types')).toBe(true)
180
+ })
181
+
182
+ await act(async () => {
183
+ await Promise.resolve()
184
+ })
185
+
186
+ const sendButton = await screen.findByRole('button', { name: /^send$/i })
187
+ fireEvent.click(sendButton)
188
+
189
+ await waitFor(() => {
190
+ const messagePostCalls = (apiCall as jest.Mock).mock.calls.filter(
191
+ (call) => call[0] === '/api/messages' && call[1]?.method === 'POST',
192
+ )
193
+ expect(messagePostCalls).toHaveLength(1)
194
+ })
195
+
196
+ await waitFor(() => {
197
+ expect(sendButton).toBeDisabled()
198
+ })
199
+
200
+ await act(async () => {
201
+ resolveMessagePost({
202
+ ok: true,
203
+ status: 201,
204
+ result: { id: 'message-1' },
205
+ response: { status: 201 },
206
+ })
207
+ })
208
+
209
+ await waitFor(() => {
210
+ expect(flash).toHaveBeenCalledWith('Message sent.', 'success')
211
+ })
212
+
213
+ expect(sendButton).toBeDisabled()
214
+
215
+ fireEvent.click(sendButton)
216
+
217
+ await waitFor(() => {
218
+ const messagePostCalls = (apiCall as jest.Mock).mock.calls.filter(
219
+ (call) => call[0] === '/api/messages' && call[1]?.method === 'POST',
220
+ )
221
+ expect(messagePostCalls).toHaveLength(1)
222
+ })
223
+ })
224
+
124
225
  it('cancels dialog composer without saving draft', async () => {
125
226
  const onCancel = jest.fn()
126
227
  const onOpenChange = jest.fn()
@@ -164,6 +164,8 @@ export function useMessageCompose({
164
164
  // because `keyboard.type` characters appeared to "type nowhere" — they were
165
165
  // typed correctly, then immediately wiped by the next effect run.
166
166
  const isOpenRef = React.useRef(false)
167
+ const wasOpenRef = React.useRef(false)
168
+ const submitLockReleaseRef = React.useRef<(() => void) | null>(null)
167
169
 
168
170
  const messageTypesQuery = useQuery({
169
171
  queryKey: ['messages', 'types'],
@@ -289,6 +291,28 @@ export function useMessageCompose({
289
291
  requiredActionConfig?.defaultActionType,
290
292
  ])
291
293
 
294
+ React.useEffect(() => {
295
+ if (!isOpen) {
296
+ submitLockReleaseRef.current?.()
297
+ submitLockReleaseRef.current = null
298
+ wasOpenRef.current = false
299
+ return
300
+ }
301
+
302
+ const justOpened = isOpen && !wasOpenRef.current
303
+ wasOpenRef.current = isOpen
304
+ if (!justOpened) return
305
+ submitLockReleaseRef.current?.()
306
+ submitLockReleaseRef.current = null
307
+ setSubmitting(false)
308
+ setSubmitMode('send')
309
+ }, [isOpen])
310
+
311
+ React.useEffect(() => () => {
312
+ submitLockReleaseRef.current?.()
313
+ submitLockReleaseRef.current = null
314
+ }, [])
315
+
292
316
  React.useEffect(() => {
293
317
  if (!isOpen) return
294
318
  if (variant !== 'forward') return
@@ -513,6 +537,8 @@ export function useMessageCompose({
513
537
 
514
538
  setSubmitMode(isComposeDraftSubmit ? 'draft' : 'send')
515
539
  setSubmitting(true)
540
+ let keepSubmitLock = false
541
+ let shouldReturnFalse = false
516
542
 
517
543
  try {
518
544
  let nextAttachmentIds = attachmentIds
@@ -525,44 +551,60 @@ export function useMessageCompose({
525
551
  : t('messages.errors.loadAttachmentOptionsFailed', 'Failed to load attachments.')
526
552
  setSubmitError(message)
527
553
  flash(message, 'error')
528
- return false
554
+ shouldReturnFalse = true
529
555
  }
530
556
  }
531
557
 
532
- const { endpoint, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })
558
+ if (!shouldReturnFalse) {
559
+ const { endpoint, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })
533
560
 
534
- const call = await apiCall<{ id?: string }>(endpoint, {
535
- method: 'POST',
536
- headers: { 'Content-Type': 'application/json' },
537
- body: JSON.stringify(payload),
538
- })
561
+ const call = await apiCall<{ id?: string }>(endpoint, {
562
+ method: 'POST',
563
+ headers: { 'Content-Type': 'application/json' },
564
+ body: JSON.stringify(payload),
565
+ })
539
566
 
540
- if (!call.ok) {
541
- const message = toErrorMessage(call.result) ?? t('messages.errors.sendFailed', 'Failed to send message.')
542
- setSubmitError(message)
543
- flash(message, 'error')
544
- return false
545
- }
546
-
547
- flash(operation.successMessage, 'success')
567
+ if (!call.ok) {
568
+ const message = toErrorMessage(call.result) ?? t('messages.errors.sendFailed', 'Failed to send message.')
569
+ setSubmitError(message)
570
+ flash(message, 'error')
571
+ shouldReturnFalse = true
572
+ } else {
573
+ flash(operation.successMessage, 'success')
574
+ keepSubmitLock = true
548
575
 
549
- onSuccess?.({ id: call.result?.id })
576
+ onSuccess?.({ id: call.result?.id })
550
577
 
551
- if (!inline) {
552
- onOpenChange?.(false)
578
+ if (!inline) {
579
+ onOpenChange?.(false)
580
+ }
581
+ }
553
582
  }
554
- return true
555
583
  } catch (error) {
556
584
  const message = error instanceof Error
557
585
  ? error.message
558
586
  : t('messages.errors.sendFailed', 'Failed to send message.')
559
587
  setSubmitError(message)
560
588
  flash(message, 'error')
561
- return false
589
+ shouldReturnFalse = true
562
590
  } finally {
563
- setSubmitting(false)
591
+ if (!keepSubmitLock) {
592
+ setSubmitting(false)
593
+ }
564
594
  setSubmitMode('send')
565
595
  }
596
+
597
+ if (shouldReturnFalse) {
598
+ return false
599
+ }
600
+
601
+ if (keepSubmitLock) {
602
+ return await new Promise<boolean>((resolve) => {
603
+ submitLockReleaseRef.current = () => resolve(true)
604
+ })
605
+ }
606
+
607
+ return true
566
608
  }, [
567
609
  attachmentIds,
568
610
  composeDraftOperation,