@open-mercato/core 0.6.6-develop.6255.1.a6ee4a57c1 → 0.6.6-develop.6257.1.6d0af84d26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/messages/api/[id]/conversation/archive/route.js +25 -7
- package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/read/route.js +24 -6
- package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +13 -1
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/openapi.js +3 -1
- package/dist/modules/messages/api/openapi.js.map +2 -2
- package/dist/modules/messages/commands/conversation.js +77 -0
- package/dist/modules/messages/commands/conversation.js.map +2 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js +16 -4
- package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +18 -0
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +13 -11
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +20 -9
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +43 -6
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/route.js +21 -4
- package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/openapi.js +1 -0
- package/dist/modules/perspectives/api/openapi.js.map +2 -2
- package/dist/modules/perspectives/data/validators.js +2 -0
- package/dist/modules/perspectives/data/validators.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +135 -6
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/messages/api/[id]/conversation/archive/route.ts +29 -6
- package/src/modules/messages/api/[id]/conversation/read/route.ts +29 -6
- package/src/modules/messages/api/[id]/route.ts +15 -0
- package/src/modules/messages/api/openapi.ts +2 -0
- package/src/modules/messages/commands/conversation.ts +87 -0
- package/src/modules/messages/components/MessageDetailPageClient.tsx +16 -4
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +26 -1
- package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +29 -14
- package/src/modules/messages/components/message-detail/types.ts +2 -0
- package/src/modules/messages/i18n/de.json +4 -0
- package/src/modules/messages/i18n/en.json +4 -0
- package/src/modules/messages/i18n/es.json +4 -0
- package/src/modules/messages/i18n/pl.json +4 -0
- package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +20 -9
- package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +45 -6
- package/src/modules/perspectives/api/[tableId]/route.ts +23 -1
- package/src/modules/perspectives/api/openapi.ts +1 -1
- package/src/modules/perspectives/data/validators.ts +2 -0
- package/src/modules/perspectives/services/perspectiveService.ts +171 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/commands/conversation.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { Message, MessageRecipient } from '../data/entities'\nimport { emitMessagesEvent } from '../events'\nimport { assertOrganizationAccess, type MessageScopeInput } from './shared'\n\nconst conversationCommandSchema = z.object({\n anchorMessageId: z.string().uuid(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype ConversationCommandInput = z.infer<typeof conversationCommandSchema>\n\ntype ConversationScope = {\n anchorMessageId: string\n messageIds: string[]\n recipientMessageIds: Set<string>\n senderMessageIds: Set<string>\n}\n\nasync function resolveConversationScope(\n em: EntityManager,\n input: ConversationCommandInput,\n): Promise<ConversationScope> {\n const anchorMessage = await em.findOne(Message, {\n id: input.anchorMessageId,\n tenantId: input.tenantId,\n deletedAt: null,\n })\n\n if (!anchorMessage) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, anchorMessage)\n\n const anchorRecipient = await em.findOne(MessageRecipient, {\n messageId: input.anchorMessageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n\n const canAccessAnchor = anchorMessage.senderUserId === input.userId || Boolean(anchorRecipient)\n if (!canAccessAnchor) throw new Error('Access denied')\n\n const threadId = anchorMessage.threadId ?? anchorMessage.id\n const conversationMessages = await em.find(\n Message,\n {\n threadId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n {\n orderBy: {\n sentAt: 'ASC',\n createdAt: 'ASC',\n id: 'ASC',\n },\n },\n )\n\n const messagesById = new Map<string, Message>(conversationMessages.map((message) => [message.id, message]))\n messagesById.set(anchorMessage.id, anchorMessage)\n\n if (anchorMessage.threadId && !messagesById.has(anchorMessage.threadId)) {\n const threadRoot = await em.findOne(Message, {\n id: anchorMessage.threadId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n })\n if (threadRoot) {\n messagesById.set(threadRoot.id, threadRoot)\n }\n }\n\n let parentMessageId = anchorMessage.parentMessageId ?? null\n while (parentMessageId) {\n if (messagesById.has(parentMessageId)) break\n const parentMessage = await em.findOne(Message, {\n id: parentMessageId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n })\n if (!parentMessage) break\n messagesById.set(parentMessage.id, parentMessage)\n parentMessageId = parentMessage.parentMessageId ?? null\n }\n\n let frontier = Array.from(messagesById.keys())\n while (frontier.length > 0) {\n const children = await em.find(Message, {\n parentMessageId: { $in: frontier },\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n })\n\n const nextFrontier: string[] = []\n for (const child of children) {\n if (messagesById.has(child.id)) continue\n messagesById.set(child.id, child)\n nextFrontier.push(child.id)\n }\n frontier = nextFrontier\n }\n\n const scopedMessages = Array.from(messagesById.values())\n const messageIds = scopedMessages.map((message) => message.id)\n const recipientRows = messageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: messageIds },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n : []\n\n const recipientMessageIds = new Set(recipientRows.map((item) => item.messageId))\n const senderMessageIds = new Set(\n scopedMessages\n .filter((item) => item.senderUserId === input.userId)\n .map((item) => item.id),\n )\n\n const visibleMessageIds = scopedMessages\n .map((item) => item.id)\n .filter((messageId) => recipientMessageIds.has(messageId) || senderMessageIds.has(messageId))\n\n if (visibleMessageIds.length === 0) throw new Error('Access denied')\n\n return {\n anchorMessageId: anchorMessage.id,\n messageIds: visibleMessageIds,\n recipientMessageIds,\n senderMessageIds,\n }\n}\n\nconst archiveConversationForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.archive_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToArchive = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))\n\n if (messageIdsToArchive.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const archivedAt = new Date()\n const archivedIds: string[] = []\n await em.transactional(async (trx) => {\n const recipients = await trx.find(MessageRecipient, {\n messageId: { $in: messageIdsToArchive },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n for (const recipient of recipients) {\n if (recipient.status !== 'archived' || recipient.archivedAt === null) {\n recipient.archivedAt = archivedAt\n recipient.status = 'archived'\n archivedIds.push(recipient.messageId)\n }\n }\n })\n\n for (const messageId of archivedIds) {\n await emitMessagesEvent('messages.message.archived', {\n messageId,\n recipientUserId: input.userId,\n userId: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: archivedIds.length }\n },\n}\n\nconst markConversationUnreadForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.mark_unread_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToMarkUnread = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))\n\n if (messageIdsToMarkUnread.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const markedIds: string[] = []\n await em.transactional(async (trx) => {\n const recipients = await trx.find(MessageRecipient, {\n messageId: { $in: messageIdsToMarkUnread },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n for (const recipient of recipients) {\n if (recipient.status !== 'unread' || recipient.readAt !== null) {\n recipient.status = 'unread'\n recipient.readAt = null\n markedIds.push(recipient.messageId)\n }\n }\n })\n\n for (const messageId of markedIds) {\n await emitMessagesEvent('messages.message.marked_unread', {\n messageId,\n recipientUserId: input.userId,\n userId: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: markedIds.length }\n },\n}\n\ntype DeletedTarget = { messageId: string; target: 'sender' | 'recipient' }\n\nconst deleteConversationForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.delete_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToDelete = scope.messageIds.filter(\n (messageId) => scope.recipientMessageIds.has(messageId) || scope.senderMessageIds.has(messageId),\n )\n\n if (messageIdsToDelete.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const deletedNow = new Date()\n const deletedTargets: DeletedTarget[] = []\n await em.transactional(async (trx) => {\n for (const messageId of messageIdsToDelete) {\n const recipient = await trx.findOne(MessageRecipient, {\n messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (recipient) {\n recipient.status = 'deleted'\n recipient.deletedAt = deletedNow\n deletedTargets.push({ messageId, target: 'recipient' })\n continue\n }\n if (scope.senderMessageIds.has(messageId)) {\n const message = await trx.findOne(Message, {\n id: messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n })\n if (message && message.senderUserId === input.userId) {\n message.deletedAt = deletedNow\n deletedTargets.push({ messageId, target: 'sender' })\n }\n }\n }\n })\n\n for (const { messageId, target } of deletedTargets) {\n await emitMessagesEvent('messages.message.deleted', {\n messageId,\n actorUserId: input.userId,\n recipientUserId: input.userId,\n target,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: deletedTargets.length }\n },\n}\n\nregisterCommand(archiveConversationForActorCommand)\nregisterCommand(markConversationUnreadForActorCommand)\nregisterCommand(deleteConversationForActorCommand)\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { Message, MessageRecipient } from '../data/entities'\nimport { emitMessagesEvent } from '../events'\nimport { assertOrganizationAccess, type MessageScopeInput } from './shared'\n\nconst conversationCommandSchema = z.object({\n anchorMessageId: z.string().uuid(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype ConversationCommandInput = z.infer<typeof conversationCommandSchema>\n\ntype ConversationScope = {\n anchorMessageId: string\n messageIds: string[]\n recipientMessageIds: Set<string>\n senderMessageIds: Set<string>\n}\n\nasync function resolveConversationScope(\n em: EntityManager,\n input: ConversationCommandInput,\n): Promise<ConversationScope> {\n const anchorMessage = await em.findOne(Message, {\n id: input.anchorMessageId,\n tenantId: input.tenantId,\n deletedAt: null,\n })\n\n if (!anchorMessage) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, anchorMessage)\n\n const anchorRecipient = await em.findOne(MessageRecipient, {\n messageId: input.anchorMessageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n\n const canAccessAnchor = anchorMessage.senderUserId === input.userId || Boolean(anchorRecipient)\n if (!canAccessAnchor) throw new Error('Access denied')\n\n const threadId = anchorMessage.threadId ?? anchorMessage.id\n const conversationMessages = await em.find(\n Message,\n {\n threadId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n {\n orderBy: {\n sentAt: 'ASC',\n createdAt: 'ASC',\n id: 'ASC',\n },\n },\n )\n\n const messagesById = new Map<string, Message>(conversationMessages.map((message) => [message.id, message]))\n messagesById.set(anchorMessage.id, anchorMessage)\n\n if (anchorMessage.threadId && !messagesById.has(anchorMessage.threadId)) {\n const threadRoot = await em.findOne(Message, {\n id: anchorMessage.threadId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n })\n if (threadRoot) {\n messagesById.set(threadRoot.id, threadRoot)\n }\n }\n\n let parentMessageId = anchorMessage.parentMessageId ?? null\n while (parentMessageId) {\n if (messagesById.has(parentMessageId)) break\n const parentMessage = await em.findOne(Message, {\n id: parentMessageId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n })\n if (!parentMessage) break\n messagesById.set(parentMessage.id, parentMessage)\n parentMessageId = parentMessage.parentMessageId ?? null\n }\n\n let frontier = Array.from(messagesById.keys())\n while (frontier.length > 0) {\n const children = await em.find(Message, {\n parentMessageId: { $in: frontier },\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n deletedAt: null,\n isDraft: false,\n })\n\n const nextFrontier: string[] = []\n for (const child of children) {\n if (messagesById.has(child.id)) continue\n messagesById.set(child.id, child)\n nextFrontier.push(child.id)\n }\n frontier = nextFrontier\n }\n\n const scopedMessages = Array.from(messagesById.values())\n const messageIds = scopedMessages.map((message) => message.id)\n const recipientRows = messageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: messageIds },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n : []\n\n const recipientMessageIds = new Set(recipientRows.map((item) => item.messageId))\n const senderMessageIds = new Set(\n scopedMessages\n .filter((item) => item.senderUserId === input.userId)\n .map((item) => item.id),\n )\n\n const visibleMessageIds = scopedMessages\n .map((item) => item.id)\n .filter((messageId) => recipientMessageIds.has(messageId) || senderMessageIds.has(messageId))\n\n if (visibleMessageIds.length === 0) throw new Error('Access denied')\n\n return {\n anchorMessageId: anchorMessage.id,\n messageIds: visibleMessageIds,\n recipientMessageIds,\n senderMessageIds,\n }\n}\n\nconst archiveConversationForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.archive_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToArchive = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))\n\n if (messageIdsToArchive.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const archivedAt = new Date()\n const archivedIds: string[] = []\n await em.transactional(async (trx) => {\n const recipients = await trx.find(MessageRecipient, {\n messageId: { $in: messageIdsToArchive },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n for (const recipient of recipients) {\n if (recipient.status !== 'archived' || recipient.archivedAt === null) {\n recipient.archivedAt = archivedAt\n recipient.status = 'archived'\n archivedIds.push(recipient.messageId)\n }\n }\n })\n\n for (const messageId of archivedIds) {\n await emitMessagesEvent('messages.message.archived', {\n messageId,\n recipientUserId: input.userId,\n userId: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: archivedIds.length }\n },\n}\n\nconst markConversationUnreadForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.mark_unread_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToMarkUnread = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))\n\n if (messageIdsToMarkUnread.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const markedIds: string[] = []\n await em.transactional(async (trx) => {\n const recipients = await trx.find(MessageRecipient, {\n messageId: { $in: messageIdsToMarkUnread },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n for (const recipient of recipients) {\n if (recipient.status !== 'unread' || recipient.readAt !== null) {\n recipient.status = 'unread'\n recipient.readAt = null\n markedIds.push(recipient.messageId)\n }\n }\n })\n\n for (const messageId of markedIds) {\n await emitMessagesEvent('messages.message.marked_unread', {\n messageId,\n recipientUserId: input.userId,\n userId: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: markedIds.length }\n },\n}\n\nconst unarchiveConversationForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.unarchive_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToUnarchive = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))\n\n if (messageIdsToUnarchive.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const unarchivedIds: string[] = []\n await em.transactional(async (trx) => {\n const recipients = await trx.find(MessageRecipient, {\n messageId: { $in: messageIdsToUnarchive },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n for (const recipient of recipients) {\n if (recipient.status === 'archived' || recipient.archivedAt !== null) {\n recipient.archivedAt = null\n recipient.status = recipient.readAt ? 'read' : 'unread'\n unarchivedIds.push(recipient.messageId)\n }\n }\n })\n\n for (const messageId of unarchivedIds) {\n await emitMessagesEvent('messages.message.unarchived', {\n messageId,\n recipientUserId: input.userId,\n userId: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: unarchivedIds.length }\n },\n}\n\nconst markConversationReadForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.mark_read_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToMarkRead = scope.messageIds.filter((messageId) => scope.recipientMessageIds.has(messageId))\n\n if (messageIdsToMarkRead.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const readAt = new Date()\n const markedIds: string[] = []\n await em.transactional(async (trx) => {\n const recipients = await trx.find(MessageRecipient, {\n messageId: { $in: messageIdsToMarkRead },\n recipientUserId: input.userId,\n deletedAt: null,\n })\n for (const recipient of recipients) {\n if (recipient.status === 'unread') {\n recipient.status = 'read'\n recipient.readAt = readAt\n markedIds.push(recipient.messageId)\n }\n }\n })\n\n for (const messageId of markedIds) {\n await emitMessagesEvent('messages.message.read', {\n messageId,\n recipientUserId: input.userId,\n userId: input.userId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: markedIds.length }\n },\n}\n\ntype DeletedTarget = { messageId: string; target: 'sender' | 'recipient' }\n\nconst deleteConversationForActorCommand: CommandHandler<unknown, { ok: true; affectedCount: number }> = {\n id: 'messages.conversation.delete_for_actor',\n async execute(rawInput, ctx) {\n const input = conversationCommandSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = await resolveConversationScope(em, input)\n const messageIdsToDelete = scope.messageIds.filter(\n (messageId) => scope.recipientMessageIds.has(messageId) || scope.senderMessageIds.has(messageId),\n )\n\n if (messageIdsToDelete.length === 0) {\n return { ok: true, affectedCount: 0 }\n }\n\n const deletedNow = new Date()\n const deletedTargets: DeletedTarget[] = []\n await em.transactional(async (trx) => {\n for (const messageId of messageIdsToDelete) {\n const recipient = await trx.findOne(MessageRecipient, {\n messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (recipient) {\n recipient.status = 'deleted'\n recipient.deletedAt = deletedNow\n deletedTargets.push({ messageId, target: 'recipient' })\n continue\n }\n if (scope.senderMessageIds.has(messageId)) {\n const message = await trx.findOne(Message, {\n id: messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n })\n if (message && message.senderUserId === input.userId) {\n message.deletedAt = deletedNow\n deletedTargets.push({ messageId, target: 'sender' })\n }\n }\n }\n })\n\n for (const { messageId, target } of deletedTargets) {\n await emitMessagesEvent('messages.message.deleted', {\n messageId,\n actorUserId: input.userId,\n recipientUserId: input.userId,\n target,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, { persistent: true })\n }\n\n return { ok: true, affectedCount: deletedTargets.length }\n },\n}\n\nregisterCommand(archiveConversationForActorCommand)\nregisterCommand(unarchiveConversationForActorCommand)\nregisterCommand(markConversationUnreadForActorCommand)\nregisterCommand(markConversationReadForActorCommand)\nregisterCommand(deleteConversationForActorCommand)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -189,6 +189,8 @@ function MessageDetailPageClient({ id }) {
|
|
|
189
189
|
const firstConversationMessageId = state.conversationItems[0]?.id ?? null;
|
|
190
190
|
const latestConversationMessageId = state.conversationItems[state.conversationItems.length - 1]?.id ?? null;
|
|
191
191
|
const canRunConversationActions = Boolean(firstConversationMessageId);
|
|
192
|
+
const conversationArchived = Boolean(detail.conversationArchived);
|
|
193
|
+
const conversationAllUnread = Boolean(detail.conversationAllUnread);
|
|
192
194
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
193
195
|
/* @__PURE__ */ jsx(
|
|
194
196
|
MainMessageHeader,
|
|
@@ -197,6 +199,8 @@ function MessageDetailPageClient({ id }) {
|
|
|
197
199
|
priority: detail.priority ?? "normal",
|
|
198
200
|
canReply: !detail.isDraft && detail.typeDefinition.allowReply && Boolean(firstConversationMessageId),
|
|
199
201
|
canForwardAll: !detail.isDraft && detail.typeDefinition.allowForward && Boolean(latestConversationMessageId),
|
|
202
|
+
conversationArchived,
|
|
203
|
+
conversationAllUnread,
|
|
200
204
|
actionsDisabled: Boolean(state.activeConversationAction),
|
|
201
205
|
activeActionId: state.activeConversationAction,
|
|
202
206
|
onReply: () => {
|
|
@@ -213,13 +217,21 @@ function MessageDetailPageClient({ id }) {
|
|
|
213
217
|
messageId: latestConversationMessageId
|
|
214
218
|
});
|
|
215
219
|
},
|
|
216
|
-
|
|
220
|
+
onToggleArchiveConversation: () => {
|
|
217
221
|
if (!canRunConversationActions) return;
|
|
218
|
-
|
|
222
|
+
if (conversationArchived) {
|
|
223
|
+
void state.unarchiveConversation(firstConversationMessageId ?? void 0);
|
|
224
|
+
} else {
|
|
225
|
+
void state.archiveConversation(firstConversationMessageId ?? void 0);
|
|
226
|
+
}
|
|
219
227
|
},
|
|
220
|
-
|
|
228
|
+
onToggleReadConversation: () => {
|
|
221
229
|
if (!canRunConversationActions) return;
|
|
222
|
-
|
|
230
|
+
if (conversationAllUnread) {
|
|
231
|
+
void state.markConversationRead(firstConversationMessageId ?? void 0);
|
|
232
|
+
} else {
|
|
233
|
+
void state.markConversationUnread(firstConversationMessageId ?? void 0);
|
|
234
|
+
}
|
|
223
235
|
},
|
|
224
236
|
onDeleteConversation: () => {
|
|
225
237
|
if (!canRunConversationActions || state.activeConversationAction) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/components/MessageDetailPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\n// UMES extension surface \u2014 message detail injection spots (SPEC-045d \u00A79.3a)\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport {\n getMessageUiComponentRegistry,\n} from './utils/typeUiRegistry'\nimport {\n MessageDetailActionsSection,\n MessageDetailAttachmentsSection,\n MessageDetailBodySection,\n MessageDetailComposerDialogs,\n MessageDetailDialogs,\n MainMessageHeader,\n MessageListComponent,\n MessageHeader,\n MessageDetailMetaSection,\n MessageDetailObjectsSection,\n} from './message-detail/detail-panels'\nimport { useMessageDetails } from './message-detail/hooks/useMessageDetails'\n\nfunction MessageConversationDetailItem({\n messageId,\n isCollapsible,\n isExpanded,\n onToggle,\n onReply,\n onForward,\n userFeatures,\n}: {\n messageId: string\n isCollapsible: boolean\n isExpanded: boolean\n onToggle: () => void\n onReply: (messageId: string) => void\n onForward: (messageId: string) => void\n userFeatures: string[]\n}) {\n const state = useMessageDetails(messageId)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n\n if (state.isLoadingDetail) {\n return (\n <section className=\"py-3\">\n <LoadingMessage label={state.t('messages.loading.detail', 'Loading message...')} />\n </section>\n )\n }\n\n if (!state.detail) {\n return (\n <section className=\"py-3\">\n <ErrorMessage label={state.loadErrorMessage} />\n </section>\n )\n }\n\n const ContentComponent = state.contentComponentKey\n ? messageUiRegistry.contentComponents[state.contentComponentKey] ?? null\n : null\n const ActionsComponent = state.actionsComponentKey\n ? messageUiRegistry.actionsComponents[state.actionsComponentKey] ?? null\n : null\n\n return (\n <section className=\"py-3\">\n <div className=\"space-y-4\">\n <section className=\"space-y-2 py-2\">\n <MessageHeader\n detail={state.detail}\n updatingState={state.updatingState}\n isArchived={state.isArchived}\n showSubject={false}\n collapseToggle={isCollapsible ? { expanded: isExpanded, onToggle } : undefined}\n onReply={() => onReply(messageId)}\n onForward={() => onForward(messageId)}\n onEdit={() => state.setEditOpen(true)}\n onToggleRead={() => void state.toggleRead()}\n onToggleArchive={() => void state.toggleArchive()}\n onDelete={() => state.setDeleteConfirmationOpen(true)}\n />\n\n <MessageDetailBodySection\n detail={state.detail}\n contentProps={state.contentProps}\n ContentComponent={ContentComponent}\n />\n\n {/* UMES \u2014 channel payload renderer mounts here (channel-linked emails, Slack blocks, etc.). */}\n <InjectionSpot\n spotId=\"detail:messages:message:body:after\"\n context={{ messageId }}\n data={state.detail}\n />\n\n <MessageDetailMetaSection detail={state.detail} />\n\n {/* UMES \u2014 channel sidebar widgets (channel info panel, contact preview, delivery status).\n `userFeatures` lets feature-gated widgets (e.g. the channel reassignment editor,\n gated on `communication_channels.assign`) render their controls for permitted users. */}\n <InjectionSpot\n spotId=\"detail:messages:message:sidebar\"\n context={{ messageId, userFeatures }}\n data={state.detail}\n />\n </section>\n\n <MessageDetailActionsSection\n detail={state.detail}\n messageActions={state.messageActions}\n executingActionId={state.executingActionId}\n ActionsComponent={ActionsComponent}\n onExecuteActionById={state.handleExecuteActionById}\n onExecuteAction={state.handleExecuteAction}\n />\n\n <MessageDetailObjectsSection\n detail={state.detail}\n objectActionsByObjectId={state.objectActionsByObjectId}\n onExecuteAction={state.handleExecuteAction}\n />\n\n <MessageDetailAttachmentsSection\n attachmentsQuery={state.attachmentsQuery}\n attachments={state.attachments}\n />\n </div>\n\n <MessageDetailComposerDialogs\n id={messageId}\n detail={state.detail}\n attachments={state.attachments}\n editOpen={state.editOpen}\n setEditOpen={state.setEditOpen}\n onRefresh={() => state.detailQuery.refetch()}\n />\n\n <MessageDetailDialogs\n pendingActionConfirmation={state.pendingActionConfirmation}\n setPendingActionConfirmation={state.setPendingActionConfirmation}\n executingActionId={state.executingActionId}\n handleConfirmPendingAction={state.handleConfirmPendingAction}\n handleActionConfirmDialogKeyDown={state.handleActionConfirmDialogKeyDown}\n deleteConfirmationOpen={state.deleteConfirmationOpen}\n setDeleteConfirmationOpen={state.setDeleteConfirmationOpen}\n updatingState={state.updatingState}\n handleDelete={state.handleDelete}\n handleDeleteDialogKeyDown={state.handleDeleteDialogKeyDown}\n />\n </section>\n )\n}\n\nexport function MessageDetailPageClient({ id }: { id: string }) {\n const state = useMessageDetails(id)\n // Resolve the viewer's feature grants once and forward them into the sidebar\n // injection spot so feature-gated widgets (channel reassignment editor) can\n // render their controls. Without this, hub widgets that gate on\n // `context.userFeatures` (e.g. `communication_channels.assign`) stay read-only.\n const [userFeatures, setUserFeatures] = React.useState<string[]>([])\n React.useEffect(() => {\n let cancelled = false\n void (async () => {\n const res = await apiCall<{ ok: boolean; granted?: string[] }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['communication_channels.assign'] }),\n }).catch(() => null)\n if (!cancelled && res?.ok && Array.isArray(res.result?.granted)) {\n setUserFeatures(res.result.granted)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [])\n const [activeInlineComposer, setActiveInlineComposer] = React.useState<{\n variant: 'reply' | 'forward'\n messageId: string\n } | null>(null)\n const inlineComposerContainerRef = React.useRef<HTMLDivElement | null>(null)\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n React.useEffect(() => {\n if (!activeInlineComposer) return\n\n const composerContainer = inlineComposerContainerRef.current\n if (!composerContainer) return\n\n const firstFocusableElement = composerContainer.querySelector<HTMLElement>(\n 'textarea, input, [contenteditable=\"true\"]',\n )\n firstFocusableElement?.focus()\n }, [activeInlineComposer])\n\n if (state.isLoadingDetail) {\n return <LoadingMessage label={state.t('messages.loading.detail', 'Loading message...')} />\n }\n\n if (!state.detail) {\n return (\n <ErrorMessage\n label={state.loadErrorMessage}\n action={(\n <Button type=\"button\" variant=\"outline\" onClick={state.backToList}>\n {state.t('messages.actions.backToList', 'Back to messages')}\n </Button>\n )}\n />\n )\n }\n\n const detail = state.detail\n const firstConversationMessageId = state.conversationItems[0]?.id ?? null\n const latestConversationMessageId = state.conversationItems[state.conversationItems.length - 1]?.id ?? null\n const canRunConversationActions = Boolean(firstConversationMessageId)\n\n return (\n <div className=\"space-y-3\">\n <MainMessageHeader\n subject={detail.subject}\n priority={(detail.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal'}\n canReply={!detail.isDraft && detail.typeDefinition.allowReply && Boolean(firstConversationMessageId)}\n canForwardAll={!detail.isDraft && detail.typeDefinition.allowForward && Boolean(latestConversationMessageId)}\n actionsDisabled={Boolean(state.activeConversationAction)}\n activeActionId={state.activeConversationAction}\n onReply={() => {\n if (!firstConversationMessageId) return\n setActiveInlineComposer({\n variant: 'reply',\n messageId: firstConversationMessageId,\n })\n }}\n onForwardAll={() => {\n if (!latestConversationMessageId) return\n setActiveInlineComposer({\n variant: 'forward',\n messageId: latestConversationMessageId,\n })\n }}\n onArchiveConversation={() => {\n if (!canRunConversationActions) return\n void state.archiveConversation(firstConversationMessageId ?? undefined)\n }}\n onMarkAllUnread={() => {\n if (!canRunConversationActions) return\n void state.markConversationUnread(firstConversationMessageId ?? undefined)\n }}\n onDeleteConversation={() => {\n if (!canRunConversationActions || state.activeConversationAction) return\n void (async () => {\n const confirmed = await confirm({\n title: state.t('messages.confirm.deleteConversationTitle', 'Delete conversation'),\n text: state.t('messages.confirm.deleteConversation', 'Delete this conversation from your view?'),\n confirmText: state.t('messages.actions.deleteConversation', 'Delete conversation'),\n cancelText: state.t('common.cancel', 'Cancel'),\n variant: 'destructive',\n })\n if (!confirmed) return\n await state.deleteConversation(firstConversationMessageId ?? undefined)\n })()\n }}\n />\n <div className=\"divide-y border-y\">\n {state.conversationItems.map((item) => {\n const isForcedExpanded = item.id === state.forcedExpandedItemId\n const isExpanded = state.isConversationItemExpanded(item.id)\n if (isExpanded) {\n return (\n <MessageConversationDetailItem\n key={item.id}\n messageId={item.id}\n userFeatures={userFeatures}\n isCollapsible={!isForcedExpanded}\n isExpanded\n onToggle={() => state.toggleConversationItem(item.id)}\n onReply={(messageId) => {\n setActiveInlineComposer({\n variant: 'reply',\n messageId,\n })\n }}\n onForward={(messageId) => {\n setActiveInlineComposer({\n variant: 'forward',\n messageId,\n })\n }}\n />\n )\n }\n\n return (\n <section key={item.id} className=\"px-1 py-1\">\n <MessageListComponent\n message={state.buildConversationListItemMessage(item)}\n onClick={() => state.toggleConversationItem(item.id)}\n />\n </section>\n )\n })}\n </div>\n\n {activeInlineComposer ? (\n <div ref={inlineComposerContainerRef}>\n <MessageComposer\n inline\n inlineBackHref={null}\n variant={activeInlineComposer.variant}\n messageId={activeInlineComposer.messageId}\n onCancel={() => {\n setActiveInlineComposer(null)\n }}\n onSuccess={() => {\n setActiveInlineComposer(null)\n void state.detailQuery.refetch()\n }}\n />\n </div>\n ) : null}\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\n// UMES extension surface \u2014 message detail injection spots (SPEC-045d \u00A79.3a)\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport {\n getMessageUiComponentRegistry,\n} from './utils/typeUiRegistry'\nimport {\n MessageDetailActionsSection,\n MessageDetailAttachmentsSection,\n MessageDetailBodySection,\n MessageDetailComposerDialogs,\n MessageDetailDialogs,\n MainMessageHeader,\n MessageListComponent,\n MessageHeader,\n MessageDetailMetaSection,\n MessageDetailObjectsSection,\n} from './message-detail/detail-panels'\nimport { useMessageDetails } from './message-detail/hooks/useMessageDetails'\n\nfunction MessageConversationDetailItem({\n messageId,\n isCollapsible,\n isExpanded,\n onToggle,\n onReply,\n onForward,\n userFeatures,\n}: {\n messageId: string\n isCollapsible: boolean\n isExpanded: boolean\n onToggle: () => void\n onReply: (messageId: string) => void\n onForward: (messageId: string) => void\n userFeatures: string[]\n}) {\n const state = useMessageDetails(messageId)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n\n if (state.isLoadingDetail) {\n return (\n <section className=\"py-3\">\n <LoadingMessage label={state.t('messages.loading.detail', 'Loading message...')} />\n </section>\n )\n }\n\n if (!state.detail) {\n return (\n <section className=\"py-3\">\n <ErrorMessage label={state.loadErrorMessage} />\n </section>\n )\n }\n\n const ContentComponent = state.contentComponentKey\n ? messageUiRegistry.contentComponents[state.contentComponentKey] ?? null\n : null\n const ActionsComponent = state.actionsComponentKey\n ? messageUiRegistry.actionsComponents[state.actionsComponentKey] ?? null\n : null\n\n return (\n <section className=\"py-3\">\n <div className=\"space-y-4\">\n <section className=\"space-y-2 py-2\">\n <MessageHeader\n detail={state.detail}\n updatingState={state.updatingState}\n isArchived={state.isArchived}\n showSubject={false}\n collapseToggle={isCollapsible ? { expanded: isExpanded, onToggle } : undefined}\n onReply={() => onReply(messageId)}\n onForward={() => onForward(messageId)}\n onEdit={() => state.setEditOpen(true)}\n onToggleRead={() => void state.toggleRead()}\n onToggleArchive={() => void state.toggleArchive()}\n onDelete={() => state.setDeleteConfirmationOpen(true)}\n />\n\n <MessageDetailBodySection\n detail={state.detail}\n contentProps={state.contentProps}\n ContentComponent={ContentComponent}\n />\n\n {/* UMES \u2014 channel payload renderer mounts here (channel-linked emails, Slack blocks, etc.). */}\n <InjectionSpot\n spotId=\"detail:messages:message:body:after\"\n context={{ messageId }}\n data={state.detail}\n />\n\n <MessageDetailMetaSection detail={state.detail} />\n\n {/* UMES \u2014 channel sidebar widgets (channel info panel, contact preview, delivery status).\n `userFeatures` lets feature-gated widgets (e.g. the channel reassignment editor,\n gated on `communication_channels.assign`) render their controls for permitted users. */}\n <InjectionSpot\n spotId=\"detail:messages:message:sidebar\"\n context={{ messageId, userFeatures }}\n data={state.detail}\n />\n </section>\n\n <MessageDetailActionsSection\n detail={state.detail}\n messageActions={state.messageActions}\n executingActionId={state.executingActionId}\n ActionsComponent={ActionsComponent}\n onExecuteActionById={state.handleExecuteActionById}\n onExecuteAction={state.handleExecuteAction}\n />\n\n <MessageDetailObjectsSection\n detail={state.detail}\n objectActionsByObjectId={state.objectActionsByObjectId}\n onExecuteAction={state.handleExecuteAction}\n />\n\n <MessageDetailAttachmentsSection\n attachmentsQuery={state.attachmentsQuery}\n attachments={state.attachments}\n />\n </div>\n\n <MessageDetailComposerDialogs\n id={messageId}\n detail={state.detail}\n attachments={state.attachments}\n editOpen={state.editOpen}\n setEditOpen={state.setEditOpen}\n onRefresh={() => state.detailQuery.refetch()}\n />\n\n <MessageDetailDialogs\n pendingActionConfirmation={state.pendingActionConfirmation}\n setPendingActionConfirmation={state.setPendingActionConfirmation}\n executingActionId={state.executingActionId}\n handleConfirmPendingAction={state.handleConfirmPendingAction}\n handleActionConfirmDialogKeyDown={state.handleActionConfirmDialogKeyDown}\n deleteConfirmationOpen={state.deleteConfirmationOpen}\n setDeleteConfirmationOpen={state.setDeleteConfirmationOpen}\n updatingState={state.updatingState}\n handleDelete={state.handleDelete}\n handleDeleteDialogKeyDown={state.handleDeleteDialogKeyDown}\n />\n </section>\n )\n}\n\nexport function MessageDetailPageClient({ id }: { id: string }) {\n const state = useMessageDetails(id)\n // Resolve the viewer's feature grants once and forward them into the sidebar\n // injection spot so feature-gated widgets (channel reassignment editor) can\n // render their controls. Without this, hub widgets that gate on\n // `context.userFeatures` (e.g. `communication_channels.assign`) stay read-only.\n const [userFeatures, setUserFeatures] = React.useState<string[]>([])\n React.useEffect(() => {\n let cancelled = false\n void (async () => {\n const res = await apiCall<{ ok: boolean; granted?: string[] }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['communication_channels.assign'] }),\n }).catch(() => null)\n if (!cancelled && res?.ok && Array.isArray(res.result?.granted)) {\n setUserFeatures(res.result.granted)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [])\n const [activeInlineComposer, setActiveInlineComposer] = React.useState<{\n variant: 'reply' | 'forward'\n messageId: string\n } | null>(null)\n const inlineComposerContainerRef = React.useRef<HTMLDivElement | null>(null)\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n React.useEffect(() => {\n if (!activeInlineComposer) return\n\n const composerContainer = inlineComposerContainerRef.current\n if (!composerContainer) return\n\n const firstFocusableElement = composerContainer.querySelector<HTMLElement>(\n 'textarea, input, [contenteditable=\"true\"]',\n )\n firstFocusableElement?.focus()\n }, [activeInlineComposer])\n\n if (state.isLoadingDetail) {\n return <LoadingMessage label={state.t('messages.loading.detail', 'Loading message...')} />\n }\n\n if (!state.detail) {\n return (\n <ErrorMessage\n label={state.loadErrorMessage}\n action={(\n <Button type=\"button\" variant=\"outline\" onClick={state.backToList}>\n {state.t('messages.actions.backToList', 'Back to messages')}\n </Button>\n )}\n />\n )\n }\n\n const detail = state.detail\n const firstConversationMessageId = state.conversationItems[0]?.id ?? null\n const latestConversationMessageId = state.conversationItems[state.conversationItems.length - 1]?.id ?? null\n const canRunConversationActions = Boolean(firstConversationMessageId)\n const conversationArchived = Boolean(detail.conversationArchived)\n const conversationAllUnread = Boolean(detail.conversationAllUnread)\n\n return (\n <div className=\"space-y-3\">\n <MainMessageHeader\n subject={detail.subject}\n priority={(detail.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal'}\n canReply={!detail.isDraft && detail.typeDefinition.allowReply && Boolean(firstConversationMessageId)}\n canForwardAll={!detail.isDraft && detail.typeDefinition.allowForward && Boolean(latestConversationMessageId)}\n conversationArchived={conversationArchived}\n conversationAllUnread={conversationAllUnread}\n actionsDisabled={Boolean(state.activeConversationAction)}\n activeActionId={state.activeConversationAction}\n onReply={() => {\n if (!firstConversationMessageId) return\n setActiveInlineComposer({\n variant: 'reply',\n messageId: firstConversationMessageId,\n })\n }}\n onForwardAll={() => {\n if (!latestConversationMessageId) return\n setActiveInlineComposer({\n variant: 'forward',\n messageId: latestConversationMessageId,\n })\n }}\n onToggleArchiveConversation={() => {\n if (!canRunConversationActions) return\n if (conversationArchived) {\n void state.unarchiveConversation(firstConversationMessageId ?? undefined)\n } else {\n void state.archiveConversation(firstConversationMessageId ?? undefined)\n }\n }}\n onToggleReadConversation={() => {\n if (!canRunConversationActions) return\n if (conversationAllUnread) {\n void state.markConversationRead(firstConversationMessageId ?? undefined)\n } else {\n void state.markConversationUnread(firstConversationMessageId ?? undefined)\n }\n }}\n onDeleteConversation={() => {\n if (!canRunConversationActions || state.activeConversationAction) return\n void (async () => {\n const confirmed = await confirm({\n title: state.t('messages.confirm.deleteConversationTitle', 'Delete conversation'),\n text: state.t('messages.confirm.deleteConversation', 'Delete this conversation from your view?'),\n confirmText: state.t('messages.actions.deleteConversation', 'Delete conversation'),\n cancelText: state.t('common.cancel', 'Cancel'),\n variant: 'destructive',\n })\n if (!confirmed) return\n await state.deleteConversation(firstConversationMessageId ?? undefined)\n })()\n }}\n />\n <div className=\"divide-y border-y\">\n {state.conversationItems.map((item) => {\n const isForcedExpanded = item.id === state.forcedExpandedItemId\n const isExpanded = state.isConversationItemExpanded(item.id)\n if (isExpanded) {\n return (\n <MessageConversationDetailItem\n key={item.id}\n messageId={item.id}\n userFeatures={userFeatures}\n isCollapsible={!isForcedExpanded}\n isExpanded\n onToggle={() => state.toggleConversationItem(item.id)}\n onReply={(messageId) => {\n setActiveInlineComposer({\n variant: 'reply',\n messageId,\n })\n }}\n onForward={(messageId) => {\n setActiveInlineComposer({\n variant: 'forward',\n messageId,\n })\n }}\n />\n )\n }\n\n return (\n <section key={item.id} className=\"px-1 py-1\">\n <MessageListComponent\n message={state.buildConversationListItemMessage(item)}\n onClick={() => state.toggleConversationItem(item.id)}\n />\n </section>\n )\n })}\n </div>\n\n {activeInlineComposer ? (\n <div ref={inlineComposerContainerRef}>\n <MessageComposer\n inline\n inlineBackHref={null}\n variant={activeInlineComposer.variant}\n messageId={activeInlineComposer.messageId}\n onCancel={() => {\n setActiveInlineComposer(null)\n }}\n onSuccess={() => {\n setActiveInlineComposer(null)\n void state.detailQuery.refetch()\n }}\n />\n </div>\n ) : null}\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -122,6 +122,14 @@ function useMessageDetailsActions({
|
|
|
122
122
|
successMessage: t("messages.flash.conversationArchived", "Conversation archived.")
|
|
123
123
|
});
|
|
124
124
|
}, [id, runConversationAction, t]);
|
|
125
|
+
const unarchiveConversation = React.useCallback(async (messageId) => {
|
|
126
|
+
const targetMessageId = messageId ?? id;
|
|
127
|
+
await runConversationAction("unarchiveConversation", {
|
|
128
|
+
url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,
|
|
129
|
+
method: "DELETE",
|
|
130
|
+
successMessage: t("messages.flash.conversationUnarchived", "Conversation restored from archive.")
|
|
131
|
+
});
|
|
132
|
+
}, [id, runConversationAction, t]);
|
|
125
133
|
const markConversationUnread = React.useCallback(async (messageId) => {
|
|
126
134
|
const targetMessageId = messageId ?? id;
|
|
127
135
|
await runConversationAction("markAllUnread", {
|
|
@@ -131,6 +139,14 @@ function useMessageDetailsActions({
|
|
|
131
139
|
skipDetailAutoMarkRead: true
|
|
132
140
|
});
|
|
133
141
|
}, [id, runConversationAction, t]);
|
|
142
|
+
const markConversationRead = React.useCallback(async (messageId) => {
|
|
143
|
+
const targetMessageId = messageId ?? id;
|
|
144
|
+
await runConversationAction("markAllRead", {
|
|
145
|
+
url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,
|
|
146
|
+
method: "PUT",
|
|
147
|
+
successMessage: t("messages.flash.conversationMarkedRead", "Conversation marked read.")
|
|
148
|
+
});
|
|
149
|
+
}, [id, runConversationAction, t]);
|
|
134
150
|
const deleteConversation = React.useCallback(async (messageId) => {
|
|
135
151
|
const targetMessageId = messageId ?? id;
|
|
136
152
|
await runConversationAction("deleteConversation", {
|
|
@@ -298,8 +314,10 @@ function useMessageDetailsActions({
|
|
|
298
314
|
toggleRead,
|
|
299
315
|
toggleArchive,
|
|
300
316
|
archiveConversation,
|
|
317
|
+
unarchiveConversation,
|
|
301
318
|
deleteConversation,
|
|
302
319
|
markConversationUnread,
|
|
320
|
+
markConversationRead,
|
|
303
321
|
requestAndRefresh,
|
|
304
322
|
handleDelete,
|
|
305
323
|
handleDeleteDialogKeyDown,
|
package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map
CHANGED
|
@@ -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}\n\ntype ConversationActionKind = 'archiveConversation' | 'deleteConversation' | 'markAllUnread'\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 refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n refreshDetailWithoutAutoMarkRead,\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 toErrorMessage(call.result)\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 } 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 markConversationUnread = React.useCallback(async (messageId?: string) => {\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 skipDetailAutoMarkRead: true,\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 await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true } : undefined,\n )\n }, [detail?.isRead, id, requestAndRefresh])\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 deleteConversation,\n markConversationUnread,\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": ";AAEA,YAAY,WAAW;AAIvB,SAAS,aAAa;AACtB,SAAS,0BAA0B;AACnC,SAAS,eAAe;AAQxB,SAAS,sBAAsB,qBAAqB,sBAAsB;
|
|
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}\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 refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n refreshDetailWithoutAutoMarkRead,\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 toErrorMessage(call.result)\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 } 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 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 skipDetailAutoMarkRead: true,\n })\n }, [id, runConversationAction, 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 await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true } : undefined,\n )\n }, [detail?.isRead, id, requestAndRefresh])\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
|
}
|
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
5
|
import { FormHeader } from "@open-mercato/ui/backend/forms";
|
|
6
6
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
7
|
-
import { Archive, Forward, MailMinus, Reply, Trash2 } from "lucide-react";
|
|
7
|
+
import { Archive, ArchiveRestore, Forward, MailMinus, MailOpen, Reply, Trash2 } from "lucide-react";
|
|
8
8
|
import { PriorityBadge } from "../../utils/PriorityBadge.js";
|
|
9
9
|
function MainMessageHeader(props) {
|
|
10
10
|
const t = useT();
|
|
@@ -26,19 +26,19 @@ function MainMessageHeader(props) {
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
id: "archive-conversation",
|
|
29
|
-
label: t("messages.actions.archiveConversation", "Archive conversation"),
|
|
30
|
-
icon: Archive,
|
|
31
|
-
onSelect: props.
|
|
29
|
+
label: props.conversationArchived ? t("messages.actions.unarchiveConversation", "Unarchive conversation") : t("messages.actions.archiveConversation", "Archive conversation"),
|
|
30
|
+
icon: props.conversationArchived ? ArchiveRestore : Archive,
|
|
31
|
+
onSelect: props.onToggleArchiveConversation,
|
|
32
32
|
disabled: props.actionsDisabled,
|
|
33
|
-
loading: props.activeActionId === "archiveConversation"
|
|
33
|
+
loading: props.activeActionId === "archiveConversation" || props.activeActionId === "unarchiveConversation"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
id: "mark-all-unread",
|
|
37
|
-
label: t("messages.actions.markAllUnread", "Mark all unread"),
|
|
38
|
-
icon: MailMinus,
|
|
39
|
-
onSelect: props.
|
|
37
|
+
label: props.conversationAllUnread ? t("messages.actions.markAllRead", "Mark all read") : t("messages.actions.markAllUnread", "Mark all unread"),
|
|
38
|
+
icon: props.conversationAllUnread ? MailOpen : MailMinus,
|
|
39
|
+
onSelect: props.onToggleReadConversation,
|
|
40
40
|
disabled: props.actionsDisabled,
|
|
41
|
-
loading: props.activeActionId === "markAllUnread"
|
|
41
|
+
loading: props.activeActionId === "markAllUnread" || props.activeActionId === "markAllRead"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
id: "delete-conversation",
|
|
@@ -52,10 +52,12 @@ function MainMessageHeader(props) {
|
|
|
52
52
|
props.actionsDisabled,
|
|
53
53
|
props.canForwardAll,
|
|
54
54
|
props.canReply,
|
|
55
|
-
props.
|
|
55
|
+
props.conversationArchived,
|
|
56
|
+
props.conversationAllUnread,
|
|
57
|
+
props.onToggleArchiveConversation,
|
|
56
58
|
props.onDeleteConversation,
|
|
57
59
|
props.onForwardAll,
|
|
58
|
-
props.
|
|
60
|
+
props.onToggleReadConversation,
|
|
59
61
|
props.onReply,
|
|
60
62
|
props.activeActionId,
|
|
61
63
|
t
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormHeader, type ActionItem } from '@open-mercato/ui/backend/forms'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Archive, Forward, MailMinus, Reply, Trash2 } from 'lucide-react'\nimport { PriorityBadge } from '../../utils/PriorityBadge'\n\ntype MainMessageHeaderProps = {\n subject: string\n priority: 'low' | 'normal' | 'high' | 'urgent'\n canReply: boolean\n canForwardAll: boolean\n actionsDisabled?: boolean\n activeActionId
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormHeader, type ActionItem } from '@open-mercato/ui/backend/forms'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Archive, ArchiveRestore, Forward, MailMinus, MailOpen, Reply, Trash2 } from 'lucide-react'\nimport { PriorityBadge } from '../../utils/PriorityBadge'\n\ntype MainMessageHeaderProps = {\n subject: string\n priority: 'low' | 'normal' | 'high' | 'urgent'\n canReply: boolean\n canForwardAll: boolean\n conversationArchived: boolean\n conversationAllUnread: boolean\n actionsDisabled?: boolean\n activeActionId?:\n | 'forwardAll'\n | 'archiveConversation'\n | 'unarchiveConversation'\n | 'markAllUnread'\n | 'markAllRead'\n | 'deleteConversation'\n | null\n onReply: () => void\n onForwardAll: () => void\n onToggleArchiveConversation: () => void\n onToggleReadConversation: () => void\n onDeleteConversation: () => void\n}\n\nexport function MainMessageHeader(props: MainMessageHeaderProps) {\n const t = useT()\n\n const menuActions = React.useMemo<ActionItem[]>(() => [\n {\n id: 'reply',\n label: t('messages.reply', 'Reply'),\n icon: Reply,\n onSelect: props.onReply,\n disabled: props.actionsDisabled || !props.canReply,\n },\n {\n id: 'forward-all',\n label: t('messages.actions.forwardAll', 'Forward all'),\n icon: Forward,\n onSelect: props.onForwardAll,\n disabled: props.actionsDisabled || !props.canForwardAll,\n loading: props.activeActionId === 'forwardAll',\n },\n {\n id: 'archive-conversation',\n label: props.conversationArchived\n ? t('messages.actions.unarchiveConversation', 'Unarchive conversation')\n : t('messages.actions.archiveConversation', 'Archive conversation'),\n icon: props.conversationArchived ? ArchiveRestore : Archive,\n onSelect: props.onToggleArchiveConversation,\n disabled: props.actionsDisabled,\n loading: props.activeActionId === 'archiveConversation' || props.activeActionId === 'unarchiveConversation',\n },\n {\n id: 'mark-all-unread',\n label: props.conversationAllUnread\n ? t('messages.actions.markAllRead', 'Mark all read')\n : t('messages.actions.markAllUnread', 'Mark all unread'),\n icon: props.conversationAllUnread ? MailOpen : MailMinus,\n onSelect: props.onToggleReadConversation,\n disabled: props.actionsDisabled,\n loading: props.activeActionId === 'markAllUnread' || props.activeActionId === 'markAllRead',\n },\n {\n id: 'delete-conversation',\n label: t('messages.actions.deleteConversation', 'Delete conversation'),\n icon: Trash2,\n onSelect: props.onDeleteConversation,\n disabled: props.actionsDisabled,\n loading: props.activeActionId === 'deleteConversation',\n },\n ], [\n props.actionsDisabled,\n props.canForwardAll,\n props.canReply,\n props.conversationArchived,\n props.conversationAllUnread,\n props.onToggleArchiveConversation,\n props.onDeleteConversation,\n props.onForwardAll,\n props.onToggleReadConversation,\n props.onReply,\n props.activeActionId,\n t,\n ])\n\n return (\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/messages\"\n backLabel={t('messages.actions.backToList', 'Back to messages')}\n title={(\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"truncate\">{props.subject}</span>\n <PriorityBadge priority={props.priority} />\n </span>\n )}\n utilityActions={props.canReply ? (\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"default\"\n aria-label={t('messages.reply', 'Reply')}\n onClick={props.onReply}\n disabled={props.actionsDisabled || !props.canReply}\n >\n <Reply className=\"h-4 w-4\" aria-hidden />\n </IconButton>\n ) : undefined}\n menuTriggerMode=\"icon\"\n menuAriaLabel={t('messages.actions.conversationActions', 'Conversation actions')}\n menuActions={menuActions}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAoGQ,SACE,KADF;AAlGR,YAAY,WAAW;AACvB,SAAS,YAAY;AACrB,SAAS,kBAAmC;AAC5C,SAAS,kBAAkB;AAC3B,SAAS,SAAS,gBAAgB,SAAS,WAAW,UAAU,OAAO,cAAc;AACrF,SAAS,qBAAqB;AAyBvB,SAAS,kBAAkB,OAA+B;AAC/D,QAAM,IAAI,KAAK;AAEf,QAAM,cAAc,MAAM,QAAsB,MAAM;AAAA,IACpD;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,kBAAkB,OAAO;AAAA,MAClC,MAAM;AAAA,MACN,UAAU,MAAM;AAAA,MAChB,UAAU,MAAM,mBAAmB,CAAC,MAAM;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,+BAA+B,aAAa;AAAA,MACrD,MAAM;AAAA,MACN,UAAU,MAAM;AAAA,MAChB,UAAU,MAAM,mBAAmB,CAAC,MAAM;AAAA,MAC1C,SAAS,MAAM,mBAAmB;AAAA,IACpC;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,MAAM,uBACT,EAAE,0CAA0C,wBAAwB,IACpE,EAAE,wCAAwC,sBAAsB;AAAA,MACpE,MAAM,MAAM,uBAAuB,iBAAiB;AAAA,MACpD,UAAU,MAAM;AAAA,MAChB,UAAU,MAAM;AAAA,MAChB,SAAS,MAAM,mBAAmB,yBAAyB,MAAM,mBAAmB;AAAA,IACtF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,MAAM,wBACT,EAAE,gCAAgC,eAAe,IACjD,EAAE,kCAAkC,iBAAiB;AAAA,MACzD,MAAM,MAAM,wBAAwB,WAAW;AAAA,MAC/C,UAAU,MAAM;AAAA,MAChB,UAAU,MAAM;AAAA,MAChB,SAAS,MAAM,mBAAmB,mBAAmB,MAAM,mBAAmB;AAAA,IAChF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,uCAAuC,qBAAqB;AAAA,MACrE,MAAM;AAAA,MACN,UAAU,MAAM;AAAA,MAChB,UAAU,MAAM;AAAA,MAChB,SAAS,MAAM,mBAAmB;AAAA,IACpC;AAAA,EACF,GAAG;AAAA,IACD,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN;AAAA,EACF,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,UAAS;AAAA,MACT,WAAW,EAAE,+BAA+B,kBAAkB;AAAA,MAC9D,OACE,qBAAC,UAAK,WAAU,kCACd;AAAA,4BAAC,UAAK,WAAU,YAAY,gBAAM,SAAQ;AAAA,QAC1C,oBAAC,iBAAc,UAAU,MAAM,UAAU;AAAA,SAC3C;AAAA,MAEF,gBAAgB,MAAM,WACpB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,cAAY,EAAE,kBAAkB,OAAO;AAAA,UACvC,SAAS,MAAM;AAAA,UACf,UAAU,MAAM,mBAAmB,CAAC,MAAM;AAAA,UAE1C,8BAAC,SAAM,WAAU,WAAU,eAAW,MAAC;AAAA;AAAA,MACzC,IACE;AAAA,MACJ,iBAAgB;AAAA,MAChB,eAAe,EAAE,wCAAwC,sBAAsB;AAAA,MAC/E;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
5
6
|
import {
|
|
6
7
|
runCrudMutationGuardAfterSuccess,
|
|
7
8
|
validateCrudMutationGuard
|
|
@@ -51,15 +52,24 @@ async function DELETE(req, ctx) {
|
|
|
51
52
|
if (guardResult && !guardResult.ok) {
|
|
52
53
|
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
let deleted = false;
|
|
56
|
+
try {
|
|
57
|
+
deleted = await deleteUserPerspective(em, cache, {
|
|
58
|
+
scope: {
|
|
59
|
+
userId: auth.sub,
|
|
60
|
+
tenantId: auth.tenantId ?? null,
|
|
61
|
+
organizationId: auth.orgId ?? null
|
|
62
|
+
},
|
|
63
|
+
tableId,
|
|
64
|
+
perspectiveId,
|
|
65
|
+
request: req
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (isCrudHttpError(err)) {
|
|
69
|
+
return NextResponse.json(err.body, { status: err.status });
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
63
73
|
if (deleted && guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
64
74
|
await runCrudMutationGuardAfterSuccess(container, {
|
|
65
75
|
tenantId: auth.tenantId ?? "",
|
|
@@ -89,6 +99,7 @@ const perspectiveDeleteDoc = {
|
|
|
89
99
|
errors: [
|
|
90
100
|
{ status: 400, description: "Invalid identifiers supplied", schema: perspectivesErrorSchema },
|
|
91
101
|
{ status: 401, description: "Authentication required", schema: perspectivesErrorSchema },
|
|
102
|
+
{ status: 409, description: "Optimistic lock conflict", schema: perspectivesErrorSchema },
|
|
92
103
|
{ status: 404, description: "Perspective not found", schema: perspectivesErrorSchema }
|
|
93
104
|
]
|
|
94
105
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/perspectives/api/%5BtableId%5D/%5BperspectiveId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { deleteUserPerspective } from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; perspectiveId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n const perspectiveId = decodeParam(ctx.params?.perspectiveId).trim()\n if (!tableId || !perspectiveId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: perspectiveId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, perspectiveId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B;AAEtC,SAAS,iBAAiB,yBAAyB,iCAAiC;AAE7E,MAAM,WAAW;AAAA,EACtB,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AACrE;AAEA,MAAM,cAAc,CAAC,UAAiD;AACpE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,MAAM,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI;AAC9C,MAAI;AACF,WAAO,mBAAmB,GAAG;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,OAAO,KAAc,KAA6D;AACtG,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAE9E,QAAM,UAAU,YAAY,IAAI,QAAQ,OAAO,EAAE,KAAK;AACtD,QAAM,gBAAgB,YAAY,IAAI,QAAQ,aAAa,EAAE,KAAK;AAClE,MAAI,CAAC,WAAW,CAAC,eAAe;AAC9B,WAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3E;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,SAAS,MAA0D;AACvE,QAAI;AACF,aAAO,UAAU,QAAQ,OAAO;AAAA,IAClC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,QAAM,cAAc,MAAM,0BAA0B,WAAW;AAAA,IAC7D,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,KAAK,SAAS;AAAA,IAC9B,QAAQ,KAAK;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,eAAe,IAAI;AAAA,IACnB,gBAAgB,IAAI;AAAA,IACpB,iBAAiB,EAAE,SAAS,cAAc;AAAA,EAC5C,CAAC;AACD,MAAI,eAAe,CAAC,YAAY,IAAI;AAClC,WAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,EAC3E;AAEA,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { deleteUserPerspective } from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; perspectiveId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n const perspectiveId = decodeParam(ctx.params?.perspectiveId).trim()\n if (!tableId || !perspectiveId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: perspectiveId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, perspectiveId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let deleted = false\n try {\n deleted = await deleteUserPerspective(em, cache, {\n scope: {\n userId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n },\n tableId,\n perspectiveId,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n\n if (deleted && guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: perspectiveId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ success: true })\n}\n\nconst perspectiveDeletePathParamsSchema = z.object({\n tableId: z.string().min(1),\n perspectiveId: z.string().uuid(),\n})\n\nconst perspectiveDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete a personal perspective',\n description: 'Removes a perspective owned by the current user for the given table.',\n tags: [perspectivesTag],\n responses: [\n { status: 200, description: 'Perspective removed.', schema: perspectivesSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifiers supplied', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: perspectivesErrorSchema },\n { status: 404, description: 'Perspective not found', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Delete personal perspective',\n pathParams: perspectiveDeletePathParamsSchema,\n methods: {\n DELETE: perspectiveDeleteDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|