@open-mercato/core 0.5.1-develop.2797.c1d2a513ed → 0.5.1-develop.2802.9223828f7f
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/auth/cli.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +106 -107
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js +235 -0
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +7 -0
- package/package.json +3 -3
- package/src/modules/auth/cli.ts +1 -1
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +17 -29
- package/src/modules/messages/components/useMessagesInboxBulkActions.ts +324 -0
- package/src/modules/messages/i18n/de.json +8 -0
- package/src/modules/messages/i18n/en.json +8 -0
- package/src/modules/messages/i18n/es.json +8 -0
- package/src/modules/messages/i18n/pl.json +8 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import type { BulkAction } from '@open-mercato/ui/backend/DataTable'
|
|
7
|
+
import type { FilterValues } from '@open-mercato/ui/backend/FilterBar'
|
|
8
|
+
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
9
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
11
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
12
|
+
import { toErrorMessage } from './message-detail/utils'
|
|
13
|
+
|
|
14
|
+
export type MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'
|
|
15
|
+
|
|
16
|
+
type MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'delete'
|
|
17
|
+
|
|
18
|
+
type BulkExecutionSummary = {
|
|
19
|
+
action: MessageBulkActionId
|
|
20
|
+
total: number
|
|
21
|
+
succeeded: number
|
|
22
|
+
failed: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type MessageBulkRequestConfig = {
|
|
26
|
+
method: 'PUT' | 'DELETE'
|
|
27
|
+
buildUrl: (messageId: string) => string
|
|
28
|
+
successKey: string
|
|
29
|
+
successFallback: string
|
|
30
|
+
errorKey: string
|
|
31
|
+
errorFallback: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type MessageInboxBulkMutationContext = {
|
|
35
|
+
actionId: MessageBulkActionId
|
|
36
|
+
messageIds: string[]
|
|
37
|
+
folder: MessageFolder
|
|
38
|
+
page: number
|
|
39
|
+
search: string
|
|
40
|
+
filters: FilterValues
|
|
41
|
+
retryLastMutation: () => Promise<boolean>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type UseMessagesInboxBulkActionsInput = {
|
|
45
|
+
folder: MessageFolder
|
|
46
|
+
page: number
|
|
47
|
+
search: string
|
|
48
|
+
filterValues: FilterValues
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type MessageInboxBulkRow = {
|
|
52
|
+
id: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MESSAGE_BULK_REQUESTS: Record<MessageBulkActionId, MessageBulkRequestConfig> = {
|
|
56
|
+
markRead: {
|
|
57
|
+
method: 'PUT',
|
|
58
|
+
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,
|
|
59
|
+
successKey: 'messages.bulk.flash.markReadSuccess',
|
|
60
|
+
successFallback: '{count} messages marked as read.',
|
|
61
|
+
errorKey: 'messages.errors.stateChangeFailed',
|
|
62
|
+
errorFallback: 'Failed to update message state.',
|
|
63
|
+
},
|
|
64
|
+
markUnread: {
|
|
65
|
+
method: 'DELETE',
|
|
66
|
+
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,
|
|
67
|
+
successKey: 'messages.bulk.flash.markUnreadSuccess',
|
|
68
|
+
successFallback: '{count} messages marked as unread.',
|
|
69
|
+
errorKey: 'messages.errors.stateChangeFailed',
|
|
70
|
+
errorFallback: 'Failed to update message state.',
|
|
71
|
+
},
|
|
72
|
+
archive: {
|
|
73
|
+
method: 'PUT',
|
|
74
|
+
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,
|
|
75
|
+
successKey: 'messages.bulk.flash.archiveSuccess',
|
|
76
|
+
successFallback: '{count} messages archived.',
|
|
77
|
+
errorKey: 'messages.errors.stateChangeFailed',
|
|
78
|
+
errorFallback: 'Failed to update message state.',
|
|
79
|
+
},
|
|
80
|
+
delete: {
|
|
81
|
+
method: 'DELETE',
|
|
82
|
+
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,
|
|
83
|
+
successKey: 'messages.bulk.flash.deleteSuccess',
|
|
84
|
+
successFallback: '{count} messages deleted.',
|
|
85
|
+
errorKey: 'messages.errors.deleteFailed',
|
|
86
|
+
errorFallback: 'Failed to delete message.',
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeSelectionScopeValue(value: unknown): unknown {
|
|
91
|
+
if (value == null) return undefined
|
|
92
|
+
if (typeof value === 'string') {
|
|
93
|
+
const trimmed = value.trim()
|
|
94
|
+
return trimmed.length > 0 ? trimmed : undefined
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
const normalized = value
|
|
98
|
+
.map((item) => normalizeSelectionScopeValue(item))
|
|
99
|
+
.filter((item) => item !== undefined)
|
|
100
|
+
return normalized.length > 0 ? normalized : undefined
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === 'object') {
|
|
103
|
+
const normalizedEntries = Object.entries(value)
|
|
104
|
+
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
|
105
|
+
.map(([key, nestedValue]) => [key, normalizeSelectionScopeValue(nestedValue)] as const)
|
|
106
|
+
.filter(([, nestedValue]) => nestedValue !== undefined)
|
|
107
|
+
if (normalizedEntries.length === 0) return undefined
|
|
108
|
+
return Object.fromEntries(normalizedEntries)
|
|
109
|
+
}
|
|
110
|
+
return value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildMessageSelectionScopeKey(
|
|
114
|
+
folder: MessageFolder,
|
|
115
|
+
page: number,
|
|
116
|
+
search: string,
|
|
117
|
+
filterValues: FilterValues,
|
|
118
|
+
): string {
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
folder,
|
|
121
|
+
page,
|
|
122
|
+
search: search.trim(),
|
|
123
|
+
filters: normalizeSelectionScopeValue(filterValues) ?? {},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function runWithConcurrency<TItem>(
|
|
128
|
+
items: TItem[],
|
|
129
|
+
limit: number,
|
|
130
|
+
worker: (item: TItem) => Promise<void>,
|
|
131
|
+
): Promise<PromiseSettledResult<void>[]> {
|
|
132
|
+
if (items.length === 0) return []
|
|
133
|
+
|
|
134
|
+
const results: PromiseSettledResult<void>[] = new Array(items.length)
|
|
135
|
+
let nextIndex = 0
|
|
136
|
+
|
|
137
|
+
const runWorker = async () => {
|
|
138
|
+
while (nextIndex < items.length) {
|
|
139
|
+
const currentIndex = nextIndex
|
|
140
|
+
nextIndex += 1
|
|
141
|
+
try {
|
|
142
|
+
await worker(items[currentIndex])
|
|
143
|
+
results[currentIndex] = { status: 'fulfilled', value: undefined }
|
|
144
|
+
} catch (error) {
|
|
145
|
+
results[currentIndex] = { status: 'rejected', reason: error }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await Promise.all(
|
|
151
|
+
Array.from({ length: Math.min(limit, items.length) }, () => runWorker()),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return results
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function useMessagesInboxBulkActions<T extends MessageInboxBulkRow>({
|
|
158
|
+
folder,
|
|
159
|
+
page,
|
|
160
|
+
search,
|
|
161
|
+
filterValues,
|
|
162
|
+
}: UseMessagesInboxBulkActionsInput): {
|
|
163
|
+
bulkActions: BulkAction<T>[] | undefined
|
|
164
|
+
selectionScopeKey: string
|
|
165
|
+
injectionContext: Record<string, unknown>
|
|
166
|
+
ConfirmDialogElement: React.ReactNode
|
|
167
|
+
} {
|
|
168
|
+
const t = useT()
|
|
169
|
+
const queryClient = useQueryClient()
|
|
170
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
171
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<MessageInboxBulkMutationContext>({
|
|
172
|
+
contextId: 'messages-inbox-bulk-actions',
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const selectionScopeKey = React.useMemo(
|
|
176
|
+
() => buildMessageSelectionScopeKey(folder, page, search, filterValues),
|
|
177
|
+
[filterValues, folder, page, search],
|
|
178
|
+
)
|
|
179
|
+
const injectionContext = React.useMemo<Record<string, unknown>>(
|
|
180
|
+
() => ({
|
|
181
|
+
folder,
|
|
182
|
+
page,
|
|
183
|
+
search: search.trim(),
|
|
184
|
+
filters: filterValues,
|
|
185
|
+
retryLastMutation,
|
|
186
|
+
}),
|
|
187
|
+
[filterValues, folder, page, retryLastMutation, search],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const executeBulkAction = React.useCallback(async (
|
|
191
|
+
actionId: MessageBulkActionId,
|
|
192
|
+
selectedRows: T[],
|
|
193
|
+
): Promise<boolean> => {
|
|
194
|
+
const messageIds = selectedRows.map((row) => row.id).filter((id) => id.trim().length > 0)
|
|
195
|
+
if (messageIds.length === 0) return false
|
|
196
|
+
|
|
197
|
+
if (actionId === 'delete') {
|
|
198
|
+
const confirmed = await confirm({
|
|
199
|
+
title: t('messages.bulk.delete.title', 'Delete {count} messages?', { count: messageIds.length }),
|
|
200
|
+
description: t('messages.bulk.delete.description', 'This removes the selected messages from your view.'),
|
|
201
|
+
confirmText: t('messages.actions.delete', 'Delete'),
|
|
202
|
+
variant: 'destructive',
|
|
203
|
+
})
|
|
204
|
+
if (!confirmed) return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const requestConfig = MESSAGE_BULK_REQUESTS[actionId]
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const summary = await runMutation<BulkExecutionSummary>({
|
|
211
|
+
operation: async () => {
|
|
212
|
+
const results = await runWithConcurrency(messageIds, 5, async (messageId) => {
|
|
213
|
+
const call = await apiCall<{ ok?: boolean }>(requestConfig.buildUrl(messageId), {
|
|
214
|
+
method: requestConfig.method,
|
|
215
|
+
})
|
|
216
|
+
if (!call.ok) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
toErrorMessage(call.result)
|
|
219
|
+
?? t(requestConfig.errorKey, requestConfig.errorFallback),
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const failed = results.filter((result) => result.status === 'rejected').length
|
|
225
|
+
const succeeded = results.length - failed
|
|
226
|
+
|
|
227
|
+
if (succeeded > 0) {
|
|
228
|
+
await queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
action: actionId,
|
|
233
|
+
total: messageIds.length,
|
|
234
|
+
succeeded,
|
|
235
|
+
failed,
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
context: {
|
|
239
|
+
actionId,
|
|
240
|
+
messageIds,
|
|
241
|
+
folder,
|
|
242
|
+
page,
|
|
243
|
+
search: search.trim(),
|
|
244
|
+
filters: filterValues,
|
|
245
|
+
retryLastMutation,
|
|
246
|
+
},
|
|
247
|
+
mutationPayload: {
|
|
248
|
+
actionId,
|
|
249
|
+
messageIds,
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (summary.succeeded === 0) {
|
|
254
|
+
flash(
|
|
255
|
+
t('messages.bulk.flash.failed', 'Failed to process {count} messages.', { count: summary.failed }),
|
|
256
|
+
'error',
|
|
257
|
+
)
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (summary.failed > 0) {
|
|
262
|
+
flash(
|
|
263
|
+
t('messages.bulk.flash.partial', '{succeeded} of {total} messages processed; {failed} failed.', {
|
|
264
|
+
succeeded: summary.succeeded,
|
|
265
|
+
total: summary.total,
|
|
266
|
+
failed: summary.failed,
|
|
267
|
+
}),
|
|
268
|
+
'warning',
|
|
269
|
+
)
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
flash(
|
|
274
|
+
t(requestConfig.successKey, requestConfig.successFallback, { count: summary.succeeded }),
|
|
275
|
+
'success',
|
|
276
|
+
)
|
|
277
|
+
return true
|
|
278
|
+
} catch (error) {
|
|
279
|
+
flash(
|
|
280
|
+
error instanceof Error
|
|
281
|
+
? error.message
|
|
282
|
+
: t(requestConfig.errorKey, requestConfig.errorFallback),
|
|
283
|
+
'error',
|
|
284
|
+
)
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
}, [confirm, filterValues, folder, page, queryClient, retryLastMutation, runMutation, search, t])
|
|
288
|
+
|
|
289
|
+
const bulkActions = React.useMemo<BulkAction<T>[] | undefined>(
|
|
290
|
+
() => folder === 'inbox'
|
|
291
|
+
? [
|
|
292
|
+
{
|
|
293
|
+
id: 'messages-mark-read',
|
|
294
|
+
label: t('messages.actions.markRead', 'Mark read'),
|
|
295
|
+
onExecute: (selectedRows: T[]) => executeBulkAction('markRead', selectedRows),
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 'messages-mark-unread',
|
|
299
|
+
label: t('messages.actions.markUnread', 'Mark unread'),
|
|
300
|
+
onExecute: (selectedRows: T[]) => executeBulkAction('markUnread', selectedRows),
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'messages-archive',
|
|
304
|
+
label: t('messages.actions.archive', 'Archive'),
|
|
305
|
+
onExecute: (selectedRows: T[]) => executeBulkAction('archive', selectedRows),
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'messages-delete',
|
|
309
|
+
label: t('messages.actions.delete', 'Delete'),
|
|
310
|
+
destructive: true,
|
|
311
|
+
onExecute: (selectedRows: T[]) => executeBulkAction('delete', selectedRows),
|
|
312
|
+
},
|
|
313
|
+
]
|
|
314
|
+
: undefined,
|
|
315
|
+
[executeBulkAction, folder, t],
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
bulkActions,
|
|
320
|
+
selectionScopeKey,
|
|
321
|
+
injectionContext,
|
|
322
|
+
ConfirmDialogElement,
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"messages.badge.unread": "{count} ungelesene Nachrichten",
|
|
25
25
|
"messages.body": "Nachricht",
|
|
26
26
|
"messages.bodyFormat.toggle": "Markdown umschalten",
|
|
27
|
+
"messages.bulk.delete.description": "Dadurch werden die ausgewählten Nachrichten aus Ihrer Ansicht entfernt.",
|
|
28
|
+
"messages.bulk.delete.title": "{count} Nachrichten löschen?",
|
|
29
|
+
"messages.bulk.flash.archiveSuccess": "{count} Nachrichten archiviert.",
|
|
30
|
+
"messages.bulk.flash.deleteSuccess": "{count} Nachrichten gelöscht.",
|
|
31
|
+
"messages.bulk.flash.failed": "{count} Nachrichten konnten nicht verarbeitet werden.",
|
|
32
|
+
"messages.bulk.flash.markReadSuccess": "{count} Nachrichten als gelesen markiert.",
|
|
33
|
+
"messages.bulk.flash.markUnreadSuccess": "{count} Nachrichten als ungelesen markiert.",
|
|
34
|
+
"messages.bulk.flash.partial": "{succeeded} von {total} Nachrichten verarbeitet; {failed} fehlgeschlagen.",
|
|
27
35
|
"messages.compose": "Nachricht verfassen",
|
|
28
36
|
"messages.composeHint": "Erstelle eine Nachricht, hänge Objekte an und sende sie an ausgewählte Empfänger.",
|
|
29
37
|
"messages.composer.attachmentPicker.confirm": "Auswahl anhängen",
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"messages.badge.unread": "{count} unread messages",
|
|
25
25
|
"messages.body": "Message",
|
|
26
26
|
"messages.bodyFormat.toggle": "Toggle markdown",
|
|
27
|
+
"messages.bulk.delete.description": "This removes the selected messages from your view.",
|
|
28
|
+
"messages.bulk.delete.title": "Delete {count} messages?",
|
|
29
|
+
"messages.bulk.flash.archiveSuccess": "{count} messages archived.",
|
|
30
|
+
"messages.bulk.flash.deleteSuccess": "{count} messages deleted.",
|
|
31
|
+
"messages.bulk.flash.failed": "Failed to process {count} messages.",
|
|
32
|
+
"messages.bulk.flash.markReadSuccess": "{count} messages marked as read.",
|
|
33
|
+
"messages.bulk.flash.markUnreadSuccess": "{count} messages marked as unread.",
|
|
34
|
+
"messages.bulk.flash.partial": "{succeeded} of {total} messages processed; {failed} failed.",
|
|
27
35
|
"messages.compose": "Compose message",
|
|
28
36
|
"messages.composeHint": "Create a message, attach objects, and send it to selected recipients.",
|
|
29
37
|
"messages.composer.attachmentPicker.confirm": "Attach selected",
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"messages.badge.unread": "{count} mensajes sin leer",
|
|
25
25
|
"messages.body": "Mensaje",
|
|
26
26
|
"messages.bodyFormat.toggle": "Alternar Markdown",
|
|
27
|
+
"messages.bulk.delete.description": "Esto elimina los mensajes seleccionados de tu vista.",
|
|
28
|
+
"messages.bulk.delete.title": "¿Eliminar {count} mensajes?",
|
|
29
|
+
"messages.bulk.flash.archiveSuccess": "{count} mensajes archivados.",
|
|
30
|
+
"messages.bulk.flash.deleteSuccess": "{count} mensajes eliminados.",
|
|
31
|
+
"messages.bulk.flash.failed": "No se pudieron procesar {count} mensajes.",
|
|
32
|
+
"messages.bulk.flash.markReadSuccess": "{count} mensajes marcados como leídos.",
|
|
33
|
+
"messages.bulk.flash.markUnreadSuccess": "{count} mensajes marcados como no leídos.",
|
|
34
|
+
"messages.bulk.flash.partial": "Se procesaron {succeeded} de {total} mensajes; {failed} fallaron.",
|
|
27
35
|
"messages.compose": "Redactar mensaje",
|
|
28
36
|
"messages.composeHint": "Crea un mensaje, adjunta objetos y envíalo a los destinatarios seleccionados.",
|
|
29
37
|
"messages.composer.attachmentPicker.confirm": "Adjuntar seleccionados",
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"messages.badge.unread": "{count} nieprzeczytanych wiadomości",
|
|
25
25
|
"messages.body": "Wiadomość",
|
|
26
26
|
"messages.bodyFormat.toggle": "Przełącz Markdown",
|
|
27
|
+
"messages.bulk.delete.description": "To usuwa wybrane wiadomości z Twojego widoku.",
|
|
28
|
+
"messages.bulk.delete.title": "Usunąć {count} wiadomości?",
|
|
29
|
+
"messages.bulk.flash.archiveSuccess": "Zarchiwizowano {count} wiadomości.",
|
|
30
|
+
"messages.bulk.flash.deleteSuccess": "Usunięto {count} wiadomości.",
|
|
31
|
+
"messages.bulk.flash.failed": "Nie udało się przetworzyć {count} wiadomości.",
|
|
32
|
+
"messages.bulk.flash.markReadSuccess": "Oznaczono {count} wiadomości jako przeczytane.",
|
|
33
|
+
"messages.bulk.flash.markUnreadSuccess": "Oznaczono {count} wiadomości jako nieprzeczytane.",
|
|
34
|
+
"messages.bulk.flash.partial": "Przetworzono {succeeded} z {total} wiadomości; niepowodzeń: {failed}.",
|
|
27
35
|
"messages.compose": "Napisz wiadomość",
|
|
28
36
|
"messages.composeHint": "Utwórz wiadomość, dołącz obiekty i wyślij do wybranych odbiorców.",
|
|
29
37
|
"messages.composer.attachmentPicker.confirm": "Dołącz wybrane",
|