@open-mercato/core 0.6.6-develop.6416.1.888fffc007 → 0.6.6-develop.6419.1.a632f417c4

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 (26) hide show
  1. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js +5 -1
  2. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
  3. package/dist/modules/messages/api/[id]/route.js +25 -2
  4. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  5. package/dist/modules/messages/api/openapi.js +1 -0
  6. package/dist/modules/messages/api/openapi.js.map +2 -2
  7. package/dist/modules/messages/commands/actions.js +8 -0
  8. package/dist/modules/messages/commands/actions.js.map +2 -2
  9. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +50 -24
  10. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  11. package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js +1 -0
  12. package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js.map +2 -2
  13. package/dist/modules/messages/lib/constants.js +3 -1
  14. package/dist/modules/messages/lib/constants.js.map +2 -2
  15. package/dist/modules/sync_excel/lib/adapters/customers.js +6 -6
  16. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  17. package/package.json +7 -7
  18. package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +5 -1
  19. package/src/modules/messages/api/[id]/route.ts +30 -1
  20. package/src/modules/messages/api/openapi.ts +1 -0
  21. package/src/modules/messages/commands/actions.ts +14 -0
  22. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +53 -28
  23. package/src/modules/messages/components/message-detail/panels/composer-dialogs.tsx +1 -0
  24. package/src/modules/messages/components/message-detail/types.ts +1 -0
  25. package/src/modules/messages/lib/constants.ts +4 -0
  26. package/src/modules/sync_excel/lib/adapters/customers.ts +6 -6
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
4
+ import { apiCall, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
6
+ import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
4
7
  import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
5
- import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
6
8
  import { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from "../utils.js";
7
9
  function toStateChangeErrorMessage(payload, t) {
8
10
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
@@ -73,20 +75,33 @@ function useMessageDetailsActions({
73
75
  setUpdatingState(true);
74
76
  try {
75
77
  await runMutation({
76
- operation: async () => {
77
- const call = await apiCall(`/api/messages/${encodeURIComponent(id)}`, {
78
- method: "DELETE"
79
- });
80
- if (!call.ok) {
81
- throw new Error(toErrorMessage(call.result) ?? t("messages.errors.deleteFailed", "Failed to delete message."));
78
+ // Carry the OSS optimistic-lock header so a stale tab cannot delete a
79
+ // concurrently-changed message; throw a status-carrying error on failure
80
+ // so the 409 conflict surfaces on the shared RecordConflictBanner.
81
+ operation: () => withScopedApiRequestHeaders(
82
+ buildOptimisticLockHeader(detail?.updatedAt),
83
+ async () => {
84
+ const call = await apiCall(`/api/messages/${encodeURIComponent(id)}`, {
85
+ method: "DELETE"
86
+ });
87
+ if (!call.ok) {
88
+ throw Object.assign(
89
+ new Error(toErrorMessage(call.result) ?? t("messages.errors.deleteFailed", "Failed to delete message.")),
90
+ {
91
+ status: call.status,
92
+ ...call.result && typeof call.result === "object" ? call.result : {}
93
+ }
94
+ );
95
+ }
82
96
  }
83
- },
97
+ ),
84
98
  context: { resourceKind: "message", messageId: id, action: "delete", retryLastMutation },
85
99
  mutationPayload: { messageId: id, action: "delete" }
86
100
  });
87
101
  flash(t("messages.flash.deleted", "Message deleted."), "success");
88
102
  onDeleted();
89
103
  } catch (err) {
104
+ if (surfaceRecordConflict(err, t)) return;
90
105
  flash(
91
106
  err instanceof Error ? err.message : t("messages.errors.deleteFailed", "Failed to delete message."),
92
107
  "error"
@@ -94,7 +109,7 @@ function useMessageDetailsActions({
94
109
  } finally {
95
110
  setUpdatingState(false);
96
111
  }
97
- }, [id, onDeleted, retryLastMutation, runMutation, t]);
112
+ }, [detail?.updatedAt, id, onDeleted, retryLastMutation, runMutation, t]);
98
113
  const runConversationAction = React.useCallback(async (action, options) => {
99
114
  setActiveConversationAction(action);
100
115
  try {
@@ -187,22 +202,32 @@ function useMessageDetailsActions({
187
202
  setExecutingActionId(action.id);
188
203
  try {
189
204
  const call = await runMutation({
190
- operation: async () => {
191
- const result = await apiCall(
192
- `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
193
- {
194
- method: "POST",
195
- headers: { "Content-Type": "application/json" },
196
- body: JSON.stringify(payload ?? {})
197
- }
198
- );
199
- if (!result.ok) {
200
- throw new Error(
201
- toErrorMessage(result.result) ?? t("messages.errors.actionFailed", "Failed to execute action.")
205
+ // Carry the OSS optimistic-lock header so a stale tab cannot execute an
206
+ // action against a concurrently-changed message; throw a status-carrying
207
+ // error on failure so the 409 conflict surfaces on the shared banner.
208
+ operation: () => withScopedApiRequestHeaders(
209
+ buildOptimisticLockHeader(detail?.updatedAt),
210
+ async () => {
211
+ const result = await apiCall(
212
+ `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
213
+ {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify(payload ?? {})
217
+ }
202
218
  );
219
+ if (!result.ok) {
220
+ throw Object.assign(
221
+ new Error(toErrorMessage(result.result) ?? t("messages.errors.actionFailed", "Failed to execute action.")),
222
+ {
223
+ status: result.status,
224
+ ...result.result && typeof result.result === "object" ? result.result : {}
225
+ }
226
+ );
227
+ }
228
+ return result;
203
229
  }
204
- return result;
205
- },
230
+ ),
206
231
  context: { resourceKind: "message", messageId: id, action: "execute-action", retryLastMutation },
207
232
  mutationPayload: { messageId: id, action: "execute-action", actionId: action.id }
208
233
  });
@@ -214,6 +239,7 @@ function useMessageDetailsActions({
214
239
  }
215
240
  await detailQuery.refetch();
216
241
  } catch (err) {
242
+ if (surfaceRecordConflict(err, t)) return;
217
243
  flash(
218
244
  err instanceof Error ? err.message : t("messages.errors.actionFailed", "Failed to execute action."),
219
245
  "error"
@@ -221,7 +247,7 @@ function useMessageDetailsActions({
221
247
  } finally {
222
248
  setExecutingActionId(null);
223
249
  }
224
- }, [detailQuery, id, retryLastMutation, runMutation, t]);
250
+ }, [detail?.updatedAt, detailQuery, id, retryLastMutation, runMutation, t]);
225
251
  const handleExecuteAction = React.useCallback(async (action, payload) => {
226
252
  if (executingActionId || detail?.actionTaken) return;
227
253
  if (action.confirmRequired) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { UseQueryResult } from '@tanstack/react-query'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport type {\n ActionResult,\n MessageAction,\n MessageAttachment,\n MessageDetail,\n PendingActionConfirmation,\n} from '../types'\nimport { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../utils'\n\ntype RequestAndRefreshOptions = {\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n}\n\ntype ConversationActionKind =\n | 'archiveConversation'\n | 'unarchiveConversation'\n | 'deleteConversation'\n | 'markAllUnread'\n | 'markAllRead'\n\ntype MessageDetailMutationContext = {\n resourceKind: 'message' | 'conversation'\n messageId: string\n action: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessageDetailsActionsInput = {\n id: string\n t: TranslateFn\n detail: MessageDetail | null\n detailQuery: UseQueryResult<MessageDetail | null, Error>\n attachments: MessageAttachment[] | undefined\n isArchived: boolean\n onDeleted: () => void\n onMarkedUnread: () => void\n refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n suppressAutoMarkRead: () => void\n}\n\nfunction toStateChangeErrorMessage(payload: unknown, t: TranslateFn): string | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n const code = (payload as Record<string, unknown>).code\n if (code === 'messages_sender_archive_unsupported') {\n return t(\n 'messages.errors.archiveSenderMessageUnsupported',\n 'You cannot archive messages you sent.',\n )\n }\n }\n return toErrorMessage(payload)\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n onMarkedUnread,\n refreshDetailWithoutAutoMarkRead,\n suppressAutoMarkRead,\n}: UseMessageDetailsActionsInput) {\n const [replyOpen, setReplyOpen] = React.useState(false)\n const [forwardOpen, setForwardOpen] = React.useState(false)\n const [editOpen, setEditOpen] = React.useState(false)\n const [updatingState, setUpdatingState] = React.useState(false)\n const [executingActionId, setExecutingActionId] = React.useState<string | null>(null)\n const [pendingActionConfirmation, setPendingActionConfirmation] = React.useState<PendingActionConfirmation | null>(null)\n const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)\n const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({\n contextId: 'messages-detail-actions',\n })\n\n const requestAndRefresh = React.useCallback(async (\n url: string,\n method: 'PUT' | 'DELETE',\n options?: RequestAndRefreshOptions,\n ) => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(url, { method })\n if (!call.ok) {\n throw new Error(\n toStateChangeErrorMessage(call.result, t)\n ?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n )\n }\n if (options?.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },\n mutationPayload: { messageId: id, action: 'state-change', url, method },\n })\n options?.onSuccess?.()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const handleDelete = React.useCallback(async () => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n })\n if (!call.ok) {\n throw new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.'))\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },\n mutationPayload: { messageId: id, action: 'delete' },\n })\n flash(t('messages.flash.deleted', 'Message deleted.'), 'success')\n onDeleted()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.deleteFailed', 'Failed to delete message.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [id, onDeleted, retryLastMutation, runMutation, t])\n\n const runConversationAction = React.useCallback(async (\n action: ConversationActionKind,\n options: {\n url: string\n method: 'PUT' | 'DELETE'\n successMessage: string\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n },\n ) => {\n setActiveConversationAction(action)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n )\n }\n },\n context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },\n mutationPayload: { messageId: id, action, url: options.url, method: options.method },\n })\n\n flash(options.successMessage, 'success')\n\n if (options.onSuccess) {\n options.onSuccess()\n return\n }\n\n if (options.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n 'error',\n )\n } finally {\n setActiveConversationAction(null)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const archiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('archiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationArchived', 'Conversation archived.'),\n })\n }, [id, runConversationAction, t])\n\n const unarchiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('unarchiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationUnarchived', 'Conversation restored from archive.'),\n })\n }, [id, runConversationAction, t])\n\n const markConversationUnread = React.useCallback(async (messageId?: string) => {\n suppressAutoMarkRead()\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllUnread', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),\n onSuccess: onMarkedUnread,\n })\n }, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])\n\n const markConversationRead = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllRead', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationMarkedRead', 'Conversation marked read.'),\n })\n }, [id, runConversationAction, t])\n\n const deleteConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('deleteConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationDeleted', 'Conversation deleted.'),\n onSuccess: onDeleted,\n })\n }, [id, onDeleted, runConversationAction, t])\n\n const handleDeleteDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleDelete()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setDeleteConfirmationOpen(false)\n }\n }, [handleDelete])\n\n const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n setExecutingActionId(action.id)\n try {\n const call = await runMutation({\n operation: async () => {\n const result = await apiCall<ActionResult>(\n `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload ?? {}),\n },\n )\n\n if (!result.ok) {\n throw new Error(\n toErrorMessage(result.result)\n ?? t('messages.errors.actionFailed', 'Failed to execute action.'),\n )\n }\n\n return result\n },\n context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },\n mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },\n })\n\n flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')\n\n const redirectTarget = call.result?.result?.redirect\n if (\n typeof redirectTarget === 'string'\n && redirectTarget.trim().length > 0\n && isSafeNavigationHref(redirectTarget)\n ) {\n window.location.href = redirectTarget\n return\n }\n\n await detailQuery.refetch()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.actionFailed', 'Failed to execute action.'),\n 'error',\n )\n } finally {\n setExecutingActionId(null)\n }\n }, [detailQuery, id, retryLastMutation, runMutation, t])\n\n const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n if (executingActionId || detail?.actionTaken) return\n\n if (action.confirmRequired) {\n setPendingActionConfirmation({ action, payload })\n return\n }\n\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId])\n\n const handleConfirmPendingAction = React.useCallback(async () => {\n if (!pendingActionConfirmation || executingActionId || detail?.actionTaken) return\n const { action, payload } = pendingActionConfirmation\n setPendingActionConfirmation(null)\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId, pendingActionConfirmation])\n\n const handleActionConfirmDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleConfirmPendingAction()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setPendingActionConfirmation(null)\n }\n }, [handleConfirmPendingAction])\n\n const contentProps = React.useMemo<MessageContentProps>(() => ({\n message: {\n id: detail?.id ?? '',\n type: detail?.type ?? 'default',\n subject: detail?.subject ?? '',\n body: detail?.body ?? '',\n bodyFormat: detail?.bodyFormat ?? 'text',\n priority: (detail?.priority ?? 'normal') as 'low' | 'normal' | 'high' | 'urgent',\n sentAt: detail?.sentAt ? new Date(detail.sentAt) : null,\n senderName: detail?.senderName ?? undefined,\n senderUserId: detail?.senderUserId ?? '',\n actionData: detail?.actionData ?? null,\n actionTaken: detail?.actionTaken ?? null,\n actionTakenAt: detail?.actionTakenAt ? new Date(detail.actionTakenAt) : null,\n },\n objects: (detail?.objects ?? []).map((item) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n snapshot: item.snapshot ?? undefined,\n })),\n attachments: (attachments ?? []).map((item) => ({\n id: item.id,\n fileName: item.fileName,\n fileSize: item.fileSize,\n mimeType: item.mimeType,\n url: item.url,\n })),\n }), [attachments, detail])\n\n const handleExecuteActionById = React.useCallback<MessageActionsProps['onExecuteAction']>(async (actionId, payload) => {\n const action = detail?.actionData?.actions?.find((entry) => {\n if (parseObjectActionId(entry.id)) return false\n return entry.id === actionId\n })\n if (!action) return\n await handleExecuteAction(action, payload)\n }, [detail?.actionData?.actions, handleExecuteAction])\n\n const messageActions = React.useMemo(() => {\n return (detail?.actionData?.actions ?? []).filter((action) => !parseObjectActionId(action.id))\n }, [detail?.actionData?.actions])\n\n const objectActionsByObjectId = React.useMemo(() => {\n const byObjectId = new Map<string, Map<string, MessageAction>>()\n for (const action of detail?.actionData?.actions ?? []) {\n const parsed = parseObjectActionId(action.id)\n if (!parsed) continue\n const current = byObjectId.get(parsed.objectId) ?? new Map<string, MessageAction>()\n current.set(parsed.actionId, action)\n byObjectId.set(parsed.objectId, current)\n }\n return byObjectId\n }, [detail?.actionData?.actions])\n\n const toggleRead = React.useCallback(async () => {\n if (detail?.isRead) {\n suppressAutoMarkRead()\n }\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,\n )\n }, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])\n\n const toggleArchive = React.useCallback(async () => {\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/archive`,\n isArchived ? 'DELETE' : 'PUT',\n )\n }, [id, isArchived, requestAndRefresh])\n\n return {\n replyOpen,\n setReplyOpen,\n forwardOpen,\n setForwardOpen,\n editOpen,\n setEditOpen,\n updatingState,\n executingActionId,\n pendingActionConfirmation,\n setPendingActionConfirmation,\n deleteConfirmationOpen,\n setDeleteConfirmationOpen,\n activeConversationAction,\n toggleRead,\n toggleArchive,\n archiveConversation,\n unarchiveConversation,\n deleteConversation,\n markConversationUnread,\n markConversationRead,\n requestAndRefresh,\n handleDelete,\n handleDeleteDialogKeyDown,\n handleExecuteAction,\n handleConfirmPendingAction,\n handleActionConfirmDialogKeyDown,\n contentProps,\n handleExecuteActionById,\n messageActions,\n objectActionsByObjectId,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { UseQueryResult } from '@tanstack/react-query'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport type {\n ActionResult,\n MessageAction,\n MessageAttachment,\n MessageDetail,\n PendingActionConfirmation,\n} from '../types'\nimport { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../utils'\n\ntype RequestAndRefreshOptions = {\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n}\n\ntype ConversationActionKind =\n | 'archiveConversation'\n | 'unarchiveConversation'\n | 'deleteConversation'\n | 'markAllUnread'\n | 'markAllRead'\n\ntype MessageDetailMutationContext = {\n resourceKind: 'message' | 'conversation'\n messageId: string\n action: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessageDetailsActionsInput = {\n id: string\n t: TranslateFn\n detail: MessageDetail | null\n detailQuery: UseQueryResult<MessageDetail | null, Error>\n attachments: MessageAttachment[] | undefined\n isArchived: boolean\n onDeleted: () => void\n onMarkedUnread: () => void\n refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n suppressAutoMarkRead: () => void\n}\n\nfunction toStateChangeErrorMessage(payload: unknown, t: TranslateFn): string | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n const code = (payload as Record<string, unknown>).code\n if (code === 'messages_sender_archive_unsupported') {\n return t(\n 'messages.errors.archiveSenderMessageUnsupported',\n 'You cannot archive messages you sent.',\n )\n }\n }\n return toErrorMessage(payload)\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n onMarkedUnread,\n refreshDetailWithoutAutoMarkRead,\n suppressAutoMarkRead,\n}: UseMessageDetailsActionsInput) {\n const [replyOpen, setReplyOpen] = React.useState(false)\n const [forwardOpen, setForwardOpen] = React.useState(false)\n const [editOpen, setEditOpen] = React.useState(false)\n const [updatingState, setUpdatingState] = React.useState(false)\n const [executingActionId, setExecutingActionId] = React.useState<string | null>(null)\n const [pendingActionConfirmation, setPendingActionConfirmation] = React.useState<PendingActionConfirmation | null>(null)\n const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)\n const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({\n contextId: 'messages-detail-actions',\n })\n\n const requestAndRefresh = React.useCallback(async (\n url: string,\n method: 'PUT' | 'DELETE',\n options?: RequestAndRefreshOptions,\n ) => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(url, { method })\n if (!call.ok) {\n throw new Error(\n toStateChangeErrorMessage(call.result, t)\n ?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n )\n }\n if (options?.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },\n mutationPayload: { messageId: id, action: 'state-change', url, method },\n })\n options?.onSuccess?.()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const handleDelete = React.useCallback(async () => {\n setUpdatingState(true)\n try {\n await runMutation({\n // Carry the OSS optimistic-lock header so a stale tab cannot delete a\n // concurrently-changed message; throw a status-carrying error on failure\n // so the 409 conflict surfaces on the shared RecordConflictBanner.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(detail?.updatedAt),\n async () => {\n const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n })\n if (!call.ok) {\n throw Object.assign(\n new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.')),\n {\n status: call.status,\n ...(call.result && typeof call.result === 'object' ? call.result : {}),\n },\n )\n }\n },\n ),\n context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },\n mutationPayload: { messageId: id, action: 'delete' },\n })\n flash(t('messages.flash.deleted', 'Message deleted.'), 'success')\n onDeleted()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.deleteFailed', 'Failed to delete message.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detail?.updatedAt, id, onDeleted, retryLastMutation, runMutation, t])\n\n const runConversationAction = React.useCallback(async (\n action: ConversationActionKind,\n options: {\n url: string\n method: 'PUT' | 'DELETE'\n successMessage: string\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n },\n ) => {\n setActiveConversationAction(action)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n )\n }\n },\n context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },\n mutationPayload: { messageId: id, action, url: options.url, method: options.method },\n })\n\n flash(options.successMessage, 'success')\n\n if (options.onSuccess) {\n options.onSuccess()\n return\n }\n\n if (options.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n 'error',\n )\n } finally {\n setActiveConversationAction(null)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const archiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('archiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationArchived', 'Conversation archived.'),\n })\n }, [id, runConversationAction, t])\n\n const unarchiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('unarchiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationUnarchived', 'Conversation restored from archive.'),\n })\n }, [id, runConversationAction, t])\n\n const markConversationUnread = React.useCallback(async (messageId?: string) => {\n suppressAutoMarkRead()\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllUnread', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),\n onSuccess: onMarkedUnread,\n })\n }, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])\n\n const markConversationRead = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllRead', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationMarkedRead', 'Conversation marked read.'),\n })\n }, [id, runConversationAction, t])\n\n const deleteConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('deleteConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationDeleted', 'Conversation deleted.'),\n onSuccess: onDeleted,\n })\n }, [id, onDeleted, runConversationAction, t])\n\n const handleDeleteDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleDelete()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setDeleteConfirmationOpen(false)\n }\n }, [handleDelete])\n\n const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n setExecutingActionId(action.id)\n try {\n const call = await runMutation({\n // Carry the OSS optimistic-lock header so a stale tab cannot execute an\n // action against a concurrently-changed message; throw a status-carrying\n // error on failure so the 409 conflict surfaces on the shared banner.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(detail?.updatedAt),\n async () => {\n const result = await apiCall<ActionResult>(\n `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload ?? {}),\n },\n )\n\n if (!result.ok) {\n throw Object.assign(\n new Error(toErrorMessage(result.result) ?? t('messages.errors.actionFailed', 'Failed to execute action.')),\n {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n },\n )\n }\n\n return result\n },\n ),\n context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },\n mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },\n })\n\n flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')\n\n const redirectTarget = call.result?.result?.redirect\n if (\n typeof redirectTarget === 'string'\n && redirectTarget.trim().length > 0\n && isSafeNavigationHref(redirectTarget)\n ) {\n window.location.href = redirectTarget\n return\n }\n\n await detailQuery.refetch()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.actionFailed', 'Failed to execute action.'),\n 'error',\n )\n } finally {\n setExecutingActionId(null)\n }\n }, [detail?.updatedAt, detailQuery, id, retryLastMutation, runMutation, t])\n\n const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n if (executingActionId || detail?.actionTaken) return\n\n if (action.confirmRequired) {\n setPendingActionConfirmation({ action, payload })\n return\n }\n\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId])\n\n const handleConfirmPendingAction = React.useCallback(async () => {\n if (!pendingActionConfirmation || executingActionId || detail?.actionTaken) return\n const { action, payload } = pendingActionConfirmation\n setPendingActionConfirmation(null)\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId, pendingActionConfirmation])\n\n const handleActionConfirmDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleConfirmPendingAction()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setPendingActionConfirmation(null)\n }\n }, [handleConfirmPendingAction])\n\n const contentProps = React.useMemo<MessageContentProps>(() => ({\n message: {\n id: detail?.id ?? '',\n type: detail?.type ?? 'default',\n subject: detail?.subject ?? '',\n body: detail?.body ?? '',\n bodyFormat: detail?.bodyFormat ?? 'text',\n priority: (detail?.priority ?? 'normal') as 'low' | 'normal' | 'high' | 'urgent',\n sentAt: detail?.sentAt ? new Date(detail.sentAt) : null,\n senderName: detail?.senderName ?? undefined,\n senderUserId: detail?.senderUserId ?? '',\n actionData: detail?.actionData ?? null,\n actionTaken: detail?.actionTaken ?? null,\n actionTakenAt: detail?.actionTakenAt ? new Date(detail.actionTakenAt) : null,\n },\n objects: (detail?.objects ?? []).map((item) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n snapshot: item.snapshot ?? undefined,\n })),\n attachments: (attachments ?? []).map((item) => ({\n id: item.id,\n fileName: item.fileName,\n fileSize: item.fileSize,\n mimeType: item.mimeType,\n url: item.url,\n })),\n }), [attachments, detail])\n\n const handleExecuteActionById = React.useCallback<MessageActionsProps['onExecuteAction']>(async (actionId, payload) => {\n const action = detail?.actionData?.actions?.find((entry) => {\n if (parseObjectActionId(entry.id)) return false\n return entry.id === actionId\n })\n if (!action) return\n await handleExecuteAction(action, payload)\n }, [detail?.actionData?.actions, handleExecuteAction])\n\n const messageActions = React.useMemo(() => {\n return (detail?.actionData?.actions ?? []).filter((action) => !parseObjectActionId(action.id))\n }, [detail?.actionData?.actions])\n\n const objectActionsByObjectId = React.useMemo(() => {\n const byObjectId = new Map<string, Map<string, MessageAction>>()\n for (const action of detail?.actionData?.actions ?? []) {\n const parsed = parseObjectActionId(action.id)\n if (!parsed) continue\n const current = byObjectId.get(parsed.objectId) ?? new Map<string, MessageAction>()\n current.set(parsed.actionId, action)\n byObjectId.set(parsed.objectId, current)\n }\n return byObjectId\n }, [detail?.actionData?.actions])\n\n const toggleRead = React.useCallback(async () => {\n if (detail?.isRead) {\n suppressAutoMarkRead()\n }\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,\n )\n }, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])\n\n const toggleArchive = React.useCallback(async () => {\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/archive`,\n isArchived ? 'DELETE' : 'PUT',\n )\n }, [id, isArchived, requestAndRefresh])\n\n return {\n replyOpen,\n setReplyOpen,\n forwardOpen,\n setForwardOpen,\n editOpen,\n setEditOpen,\n updatingState,\n executingActionId,\n pendingActionConfirmation,\n setPendingActionConfirmation,\n deleteConfirmationOpen,\n setDeleteConfirmationOpen,\n activeConversationAction,\n toggleRead,\n toggleArchive,\n archiveConversation,\n unarchiveConversation,\n deleteConversation,\n markConversationUnread,\n markConversationRead,\n requestAndRefresh,\n handleDelete,\n handleDeleteDialogKeyDown,\n handleExecuteAction,\n handleConfirmPendingAction,\n handleActionConfirmDialogKeyDown,\n contentProps,\n handleExecuteActionById,\n messageActions,\n objectActionsByObjectId,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,7 @@ function MessageDetailComposerDialogs(props) {
7
7
  {
8
8
  variant: "compose",
9
9
  messageId: props.id,
10
+ expectedUpdatedAt: props.detail.updatedAt,
10
11
  open: props.editOpen && props.detail.isDraft && props.detail.canEditDraft,
11
12
  onOpenChange: props.setEditOpen,
12
13
  defaultValues: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/messages/components/message-detail/panels/composer-dialogs.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport type { MessageAttachment, MessageDetail } from '../types'\n\ntype ComposerDialogsProps = {\n id: string\n detail: MessageDetail\n attachments: MessageAttachment[] | undefined\n editOpen: boolean\n setEditOpen: (value: boolean) => void\n onRefresh: () => Promise<unknown>\n}\n\nexport function MessageDetailComposerDialogs(props: ComposerDialogsProps) {\n return (\n <MessageComposer\n variant=\"compose\"\n messageId={props.id}\n open={props.editOpen && props.detail.isDraft && props.detail.canEditDraft}\n onOpenChange={props.setEditOpen}\n defaultValues={{\n type: props.detail.type,\n recipients: props.detail.recipients.map((recipient) => recipient.userId),\n subject: props.detail.subject,\n body: props.detail.body,\n bodyFormat: props.detail.bodyFormat,\n priority: props.detail.priority as 'low' | 'normal' | 'high' | 'urgent',\n visibility: props.detail.visibility ?? 'internal',\n sourceEntityType: props.detail.sourceEntityType ?? null,\n sourceEntityId: props.detail.sourceEntityId ?? null,\n externalEmail: props.detail.externalEmail ?? null,\n externalName: props.detail.externalName ?? null,\n attachmentIds: props.attachments?.map((attachment) => attachment.id) ?? [],\n }}\n onSuccess={() => {\n props.setEditOpen(false)\n void props.onRefresh()\n }}\n />\n )\n}\n"],
5
- "mappings": ";AAgBI;AAdJ,SAAS,uBAAuB;AAYzB,SAAS,6BAA6B,OAA6B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,OAAO;AAAA,MAC7D,cAAc,MAAM;AAAA,MACpB,eAAe;AAAA,QACb,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO,WAAW,IAAI,CAAC,cAAc,UAAU,MAAM;AAAA,QACvE,SAAS,MAAM,OAAO;AAAA,QACtB,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO;AAAA,QACzB,UAAU,MAAM,OAAO;AAAA,QACvB,YAAY,MAAM,OAAO,cAAc;AAAA,QACvC,kBAAkB,MAAM,OAAO,oBAAoB;AAAA,QACnD,gBAAgB,MAAM,OAAO,kBAAkB;AAAA,QAC/C,eAAe,MAAM,OAAO,iBAAiB;AAAA,QAC7C,cAAc,MAAM,OAAO,gBAAgB;AAAA,QAC3C,eAAe,MAAM,aAAa,IAAI,CAAC,eAAe,WAAW,EAAE,KAAK,CAAC;AAAA,MAC3E;AAAA,MACA,WAAW,MAAM;AACf,cAAM,YAAY,KAAK;AACvB,aAAK,MAAM,UAAU;AAAA,MACvB;AAAA;AAAA,EACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport type { MessageAttachment, MessageDetail } from '../types'\n\ntype ComposerDialogsProps = {\n id: string\n detail: MessageDetail\n attachments: MessageAttachment[] | undefined\n editOpen: boolean\n setEditOpen: (value: boolean) => void\n onRefresh: () => Promise<unknown>\n}\n\nexport function MessageDetailComposerDialogs(props: ComposerDialogsProps) {\n return (\n <MessageComposer\n variant=\"compose\"\n messageId={props.id}\n expectedUpdatedAt={props.detail.updatedAt}\n open={props.editOpen && props.detail.isDraft && props.detail.canEditDraft}\n onOpenChange={props.setEditOpen}\n defaultValues={{\n type: props.detail.type,\n recipients: props.detail.recipients.map((recipient) => recipient.userId),\n subject: props.detail.subject,\n body: props.detail.body,\n bodyFormat: props.detail.bodyFormat,\n priority: props.detail.priority as 'low' | 'normal' | 'high' | 'urgent',\n visibility: props.detail.visibility ?? 'internal',\n sourceEntityType: props.detail.sourceEntityType ?? null,\n sourceEntityId: props.detail.sourceEntityId ?? null,\n externalEmail: props.detail.externalEmail ?? null,\n externalName: props.detail.externalName ?? null,\n attachmentIds: props.attachments?.map((attachment) => attachment.id) ?? [],\n }}\n onSuccess={() => {\n props.setEditOpen(false)\n void props.onRefresh()\n }}\n />\n )\n}\n"],
5
+ "mappings": ";AAgBI;AAdJ,SAAS,uBAAuB;AAYzB,SAAS,6BAA6B,OAA6B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,mBAAmB,MAAM,OAAO;AAAA,MAChC,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,OAAO;AAAA,MAC7D,cAAc,MAAM;AAAA,MACpB,eAAe;AAAA,QACb,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO,WAAW,IAAI,CAAC,cAAc,UAAU,MAAM;AAAA,QACvE,SAAS,MAAM,OAAO;AAAA,QACtB,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO;AAAA,QACzB,UAAU,MAAM,OAAO;AAAA,QACvB,YAAY,MAAM,OAAO,cAAc;AAAA,QACvC,kBAAkB,MAAM,OAAO,oBAAoB;AAAA,QACnD,gBAAgB,MAAM,OAAO,kBAAkB;AAAA,QAC/C,eAAe,MAAM,OAAO,iBAAiB;AAAA,QAC7C,cAAc,MAAM,OAAO,gBAAgB;AAAA,QAC3C,eAAe,MAAM,aAAa,IAAI,CAAC,eAAe,WAAW,EAAE,KAAK,CAAC;AAAA,MAC3E;AAAA,MACA,WAAW,MAAM;AACf,cAAM,YAAY,KAAK;AACvB,aAAK,MAAM,UAAU;AAAA,MACvB;AAAA;AAAA,EACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,11 @@
1
1
  const MESSAGE_ENTITY_ID = "messages:message";
2
2
  const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID;
3
3
  const MESSAGE_ATTACHMENT_PARTITION = "messages";
4
+ const MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND = "messages.message";
4
5
  export {
5
6
  MESSAGE_ATTACHMENT_ENTITY_ID,
6
7
  MESSAGE_ATTACHMENT_PARTITION,
7
- MESSAGE_ENTITY_ID
8
+ MESSAGE_ENTITY_ID,
9
+ MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND
8
10
  };
9
11
  //# sourceMappingURL=constants.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/lib/constants.ts"],
4
- "sourcesContent": ["export const MESSAGE_ENTITY_ID = 'messages:message'\nexport const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID\nexport const MESSAGE_ATTACHMENT_PARTITION = 'messages'\n"],
5
- "mappings": "AAAO,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,+BAA+B;",
4
+ "sourcesContent": ["export const MESSAGE_ENTITY_ID = 'messages:message'\nexport const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID\nexport const MESSAGE_ATTACHMENT_PARTITION = 'messages'\n\n// Resource kind used by the command-level OSS optimistic-lock guard so stale\n// draft edits and message actions fail with the structured 409 conflict.\nexport const MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND = 'messages.message'\n"],
5
+ "mappings": "AAAO,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,+BAA+B;AAIrC,MAAM,wCAAwC;",
6
6
  "names": []
7
7
  }
@@ -800,14 +800,14 @@ const syncExcelCustomersAdapter = {
800
800
  const startOffset = cursor?.uploadId === upload.id ? cursor.offset : 0;
801
801
  const commandContext = buildCommandContext(container, input.scope);
802
802
  const customFieldDefinitions = await loadImportCustomFieldDefinitions(em, input.scope);
803
+ const emailDedupeIndex = await buildEmailDedupeIndex({
804
+ em,
805
+ rows: document.rows.slice(startOffset),
806
+ mapping: input.mapping,
807
+ scope: input.scope
808
+ });
803
809
  for (let offset = startOffset, batchIndex = 0; offset < document.rows.length; offset += input.batchSize, batchIndex += 1) {
804
810
  const batchRows = document.rows.slice(offset, offset + input.batchSize);
805
- const emailDedupeIndex = await buildEmailDedupeIndex({
806
- em,
807
- rows: batchRows,
808
- mapping: input.mapping,
809
- scope: input.scope
810
- });
811
811
  const items = [];
812
812
  for (let index = 0; index < batchRows.length; index += 1) {
813
813
  items.push(await processRow({