@adatechnology/conversations-ui 0.1.0-rc.26 → 0.1.0-rc.28
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/dist/chunk-DXPSPUWF.js +110 -0
- package/dist/{chunk-TV4OQRGH.js → chunk-UZJBYD5O.js} +123 -45
- package/dist/{chunk-2AYDBWNE.js → chunk-WCBDXZ3X.js} +13 -3
- package/dist/flows/index.d.ts +75 -2
- package/dist/flows/index.js +176 -95
- package/dist/index.d.ts +37 -10
- package/dist/index.js +694 -297
- package/dist/preview/index.js +3 -3
- package/dist/styles.css +119 -6
- package/package.json +2 -2
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +3 -3
- package/src/AudioRecorderButton.tsx +3 -3
- package/src/AudioTranscription.tsx +4 -1
- package/src/Avatar.tsx +1 -1
- package/src/ConversationContextPanel.tsx +2 -2
- package/src/ConversationDocumentsPanel.tsx +11 -6
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +147 -47
- package/src/ConversationListItem.tsx +3 -2
- package/src/ConversationRow.tsx +31 -7
- package/src/DocumentsLibrary.tsx +9 -4
- package/src/EmojiPicker.tsx +2 -1
- package/src/InteractiveMessage.tsx +3 -0
- package/src/Lightbox.tsx +1 -1
- package/src/MediaRenderer.tsx +2 -0
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +27 -8
- package/src/MessageComposer.tsx +11 -8
- package/src/RichMessageComposer.test.tsx +42 -12
- package/src/RichMessageComposer.tsx +216 -45
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +164 -0
- package/src/WhatsAppMessageEditor.tsx +4 -4
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationWindow.ts +7 -5
- package/src/documents/DocumentsWorkspace.tsx +10 -6
- package/src/flows/FlowGroupHeader.tsx +2 -2
- package/src/flows/FlowMapNode.tsx +2 -1
- package/src/flows/FlowNodeCard.tsx +3 -3
- package/src/flows/FlowNodePanel.tsx +6 -5
- package/src/flows/FlowPalette.tsx +7 -1
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowsWorkspace.tsx +69 -102
- package/src/flows/flowEditorOps.test.ts +241 -0
- package/src/flows/flowEditorOps.ts +177 -0
- package/src/flows/flowGraph.ts +1 -1
- package/src/flows/index.ts +16 -0
- package/src/flows/labels.ts +4 -0
- package/src/flows/workspaceContract.test.ts +95 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +1 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +5 -1
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationSimulatorPanel.tsx +1 -1
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/settings/MessagesWorkspace.tsx +3 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +1 -1
- package/src/settings/TranscriptionSettingsForm.tsx +1 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.tsx +2 -0
- package/src/styles.css +100 -4
- package/src/workspace/BulkTemplateModal.tsx +4 -2
- package/src/workspace/ConversationPane.tsx +49 -19
- package/src/workspace/ConversationsInboxList.tsx +17 -8
- package/src/workspace/ConversationsWorkspace.tsx +24 -16
- package/src/workspace/useConversationsInbox.ts +5 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* última mensagem, outro engolia falha de anexo, outro não abria a biblioteca de arquivos.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useEffect, useState, type ReactNode } from 'react'
|
|
8
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
9
9
|
|
|
10
10
|
import { AudioRecorderButton } from '../AudioRecorderButton'
|
|
11
11
|
import { ConversationContextPanel, type ConversationContextEntry } from '../ConversationContextPanel'
|
|
@@ -36,7 +36,8 @@ export interface ConversationPaneProps {
|
|
|
36
36
|
readonly now: number
|
|
37
37
|
readonly busy: boolean
|
|
38
38
|
readonly labels: ConversationsWorkspaceLabels
|
|
39
|
-
|
|
39
|
+
/** Devolve a promessa quando o host a tem: o envio de template espera a tomada antes de sair. */
|
|
40
|
+
readonly onTakeover?: (() => void | Promise<void>) | undefined
|
|
40
41
|
readonly onReturnToBot?: (() => void) | undefined
|
|
41
42
|
readonly onFinish?: (() => void) | undefined
|
|
42
43
|
readonly onBack: () => void
|
|
@@ -139,6 +140,9 @@ export function ConversationPane({
|
|
|
139
140
|
const [selectedMessageIds, setSelectedMessageIds] = useState<ReadonlySet<string>>(new Set())
|
|
140
141
|
const [draft, setDraft] = useState(initialComposerText ?? '')
|
|
141
142
|
const [queuedFiles, setQueuedFiles] = useState<readonly File[]>([])
|
|
143
|
+
const [isSendingDraft, setIsSendingDraft] = useState(false)
|
|
144
|
+
/** Ref, não estado: entre dois cliques seguidos o React ainda não teria repintado a trava. */
|
|
145
|
+
const sendInFlightRef = useRef(false)
|
|
142
146
|
|
|
143
147
|
// Trocar de conversa zera as duas coisas: seleção de mensagem e rascunho pertencem à thread, e
|
|
144
148
|
// levá-los adiante faria copiar o trecho errado ou responder ao cliente errado.
|
|
@@ -187,18 +191,31 @@ export function ConversationPane({
|
|
|
187
191
|
* Falha vira aviso na tela em vez de exceção silenciosa: o atendente escreveu ou gravou, achou
|
|
188
192
|
* que mandou, e sem retorno não teria como saber que o cliente não recebeu nada.
|
|
189
193
|
*/
|
|
190
|
-
|
|
194
|
+
/** Devolve se o envio passou: limpar o rascunho depois de uma falha apagaria o texto do operador. */
|
|
195
|
+
async function runSend(action: () => Promise<unknown>, fallback: string): Promise<boolean> {
|
|
191
196
|
setSendFailure(undefined)
|
|
192
197
|
try {
|
|
193
198
|
await action()
|
|
194
199
|
await refetch()
|
|
200
|
+
return true
|
|
195
201
|
} catch (error: unknown) {
|
|
196
202
|
setSendFailure(error instanceof Error ? error.message : fallback)
|
|
203
|
+
return false
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
|
|
200
|
-
async function handleSend(text: string): Promise<
|
|
201
|
-
|
|
207
|
+
async function handleSend(text: string): Promise<boolean> {
|
|
208
|
+
return runSend(() => api.sendMessage(conversation.id, text), labels.sendFailure)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reabrir a janela é ato de atendente: quem manda o template quer conversar. A tomada vai antes do
|
|
213
|
+
* envio — se ficasse depois, a resposta do cliente chegaria com a conversa ainda no bot e o fluxo
|
|
214
|
+
* automático responderia por cima de quem acabou de reabri-la.
|
|
215
|
+
*/
|
|
216
|
+
async function handleSendTemplate(): Promise<void> {
|
|
217
|
+
if (conversation.mode !== 'human' && onTakeover) await onTakeover()
|
|
218
|
+
await runSend(() => api.sendTemplate(conversation.id, {}), labels.sendFailure)
|
|
202
219
|
}
|
|
203
220
|
|
|
204
221
|
/**
|
|
@@ -206,17 +223,28 @@ export function ConversationPane({
|
|
|
206
223
|
* frase embaixo, e mandar as duas separadas invertia a ordem quando a mídia demorava a subir.
|
|
207
224
|
*/
|
|
208
225
|
async function handleRichSend(): Promise<void> {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
// O upload da mídia demora e não dá retorno na tela; sem esta trava o segundo clique — ou o
|
|
227
|
+
// Enter impaciente — mandava o mesmo arquivo outra vez.
|
|
228
|
+
if (sendInFlightRef.current) return
|
|
229
|
+
sendInFlightRef.current = true
|
|
230
|
+
setIsSendingDraft(true)
|
|
231
|
+
try {
|
|
232
|
+
if (onSendAttachments && queuedFiles.length > 0) {
|
|
233
|
+
const files = queuedFiles
|
|
234
|
+
const caption = draft
|
|
235
|
+
const didSend = await runSend(() => onSendAttachments(files, caption), labels.attachFailure)
|
|
236
|
+
if (didSend) {
|
|
237
|
+
setQueuedFiles([])
|
|
238
|
+
setDraft('')
|
|
239
|
+
}
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
if (!draft.trim()) return
|
|
243
|
+
if (await handleSend(draft)) setDraft('')
|
|
244
|
+
} finally {
|
|
245
|
+
sendInFlightRef.current = false
|
|
246
|
+
setIsSendingDraft(false)
|
|
216
247
|
}
|
|
217
|
-
if (!draft.trim()) return
|
|
218
|
-
await handleSend(draft)
|
|
219
|
-
setDraft('')
|
|
220
248
|
}
|
|
221
249
|
|
|
222
250
|
async function handleAttach(file: File): Promise<void> {
|
|
@@ -309,10 +337,10 @@ export function ConversationPane({
|
|
|
309
337
|
<div className="cv-workspace-selection">
|
|
310
338
|
<span>{labels.messagesSelected(selectedMessageIds.size)}</span>
|
|
311
339
|
<div className="cv-workspace-selection__actions">
|
|
312
|
-
<button type="button" onClick={() => setSelectedMessageIds(new Set())}>
|
|
340
|
+
<button data-cv-tooltip={labels.bulkClear} aria-label={labels.bulkClear} type="button" onClick={() => setSelectedMessageIds(new Set())}>
|
|
313
341
|
{labels.bulkClear}
|
|
314
342
|
</button>
|
|
315
|
-
<button type="button" onClick={copySelectedMessages}>
|
|
343
|
+
<button data-cv-tooltip={labels.copySelected} aria-label={labels.copySelected} type="button" onClick={copySelectedMessages}>
|
|
316
344
|
{labels.copySelected}
|
|
317
345
|
</button>
|
|
318
346
|
</div>
|
|
@@ -328,7 +356,7 @@ export function ConversationPane({
|
|
|
328
356
|
{blocked ? (
|
|
329
357
|
<WindowExpiredNotice
|
|
330
358
|
disabled={busy}
|
|
331
|
-
onSendTemplate={() => void
|
|
359
|
+
onSendTemplate={() => void handleSendTemplate()}
|
|
332
360
|
/>
|
|
333
361
|
) : botOwnsConversation ? (
|
|
334
362
|
// Responder com a conversa no bot atropelaria o fluxo automático no meio de uma pergunta.
|
|
@@ -348,7 +376,8 @@ export function ConversationPane({
|
|
|
348
376
|
}
|
|
349
377
|
: {})}
|
|
350
378
|
placeholder={labels.composerPlaceholder}
|
|
351
|
-
isSending={busy}
|
|
379
|
+
isSending={busy || isSendingDraft}
|
|
380
|
+
hasQueuedAttachments={queuedFiles.length > 0}
|
|
352
381
|
{...(onRecordAudio
|
|
353
382
|
? {
|
|
354
383
|
idleAction: (
|
|
@@ -367,6 +396,7 @@ export function ConversationPane({
|
|
|
367
396
|
<li key={`${file.name}-${index}`}>
|
|
368
397
|
<span>{file.name}</span>
|
|
369
398
|
<button
|
|
399
|
+
data-cv-tooltip={labels.attachmentRemove}
|
|
370
400
|
type="button"
|
|
371
401
|
aria-label={labels.attachmentRemove}
|
|
372
402
|
onClick={() =>
|
|
@@ -14,6 +14,7 @@ import { WINDOW_FILTERS } from '../conversationWindow'
|
|
|
14
14
|
import type { ConversationSummary } from '../providers/types'
|
|
15
15
|
import type { ConversationsWorkspaceLabels } from './labels'
|
|
16
16
|
import type { UseConversationsInboxResult } from './useConversationsInbox'
|
|
17
|
+
import { PAGINATION_LABELS } from '../pagination.constant'
|
|
17
18
|
|
|
18
19
|
export interface ConversationsInboxListProps {
|
|
19
20
|
readonly inbox: UseConversationsInboxResult
|
|
@@ -44,16 +45,17 @@ export function ConversationsInboxList({
|
|
|
44
45
|
<div className="cv-workspace-bulk">
|
|
45
46
|
<span className="cv-workspace-bulk__count">{labels.bulkSelected(inbox.selectedIds.size)}</span>
|
|
46
47
|
<div className="cv-workspace-bulk__actions">
|
|
47
|
-
<button type="button" onClick={inbox.clearBulkSelection}>
|
|
48
|
+
<button data-cv-tooltip={labels.bulkClear} aria-label={labels.bulkClear} type="button" onClick={inbox.clearBulkSelection}>
|
|
48
49
|
{labels.bulkClear}
|
|
49
50
|
</button>
|
|
50
51
|
{onSendTemplateToSelected ? (
|
|
51
|
-
<button type="button" onClick={onSendTemplateToSelected} disabled={inbox.busy}>
|
|
52
|
+
<button data-cv-tooltip={labels.bulkTemplate} aria-label={labels.bulkTemplate} type="button" onClick={onSendTemplateToSelected} disabled={inbox.busy}>
|
|
52
53
|
{labels.bulkTemplate}
|
|
53
54
|
</button>
|
|
54
55
|
) : null}
|
|
55
56
|
{inbox.canFinalize ? (
|
|
56
57
|
<button
|
|
58
|
+
data-cv-tooltip={labels.bulkFinalize} aria-label={labels.bulkFinalize}
|
|
57
59
|
type="button"
|
|
58
60
|
disabled={inbox.busy}
|
|
59
61
|
onClick={() => {
|
|
@@ -89,9 +91,15 @@ export function ConversationsInboxList({
|
|
|
89
91
|
type="button"
|
|
90
92
|
onClick={() => inbox.setWindowFilter(filter.value)}
|
|
91
93
|
aria-pressed={inbox.windowFilter === filter.value}
|
|
92
|
-
|
|
94
|
+
data-cv-tooltip={filter.label} aria-label={filter.label}
|
|
95
|
+
className={[
|
|
96
|
+
'cv-workspace-chip',
|
|
97
|
+
filter.tone ? `cv-workspace-chip--${filter.tone}` : '',
|
|
98
|
+
inbox.windowFilter === filter.value ? 'cv-workspace-chip--on' : '',
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(' ')}
|
|
93
102
|
>
|
|
94
|
-
{filter.dotClass ? <span className={`cv-workspace-dot ${filter.dotClass}`} /> : null}
|
|
95
103
|
{filter.label}
|
|
96
104
|
</button>
|
|
97
105
|
))}
|
|
@@ -103,6 +111,7 @@ export function ConversationsInboxList({
|
|
|
103
111
|
<span className="cv-workspace-chips__legend">{labels.channelLegend}</span>
|
|
104
112
|
{inbox.channelFilters.map((filter) => (
|
|
105
113
|
<button
|
|
114
|
+
data-cv-tooltip={filter.label} aria-label={filter.label}
|
|
106
115
|
key={filter.value}
|
|
107
116
|
type="button"
|
|
108
117
|
onClick={() => inbox.setChannelFilter(filter.value)}
|
|
@@ -150,14 +159,14 @@ export function ConversationsInboxList({
|
|
|
150
159
|
<div className="cv-workspace-pager">
|
|
151
160
|
<span>{labels.rangeOf(inbox.firstOnPage, inbox.lastOnPage, inbox.filteredCount)}</span>
|
|
152
161
|
<div className="cv-workspace-pager__buttons">
|
|
153
|
-
<button type="button" onClick={() => inbox.goToPage(1)} disabled={inbox.page === 1} aria-label=
|
|
162
|
+
<button type="button" onClick={() => inbox.goToPage(1)} disabled={inbox.page === 1} aria-label={PAGINATION_LABELS.first} data-cv-tooltip={PAGINATION_LABELS.first}>
|
|
154
163
|
«
|
|
155
164
|
</button>
|
|
156
165
|
<button
|
|
157
166
|
type="button"
|
|
158
167
|
onClick={() => inbox.goToPage(inbox.page - 1)}
|
|
159
168
|
disabled={inbox.page === 1}
|
|
160
|
-
aria-label=
|
|
169
|
+
aria-label={PAGINATION_LABELS.previous} data-cv-tooltip={PAGINATION_LABELS.previous}
|
|
161
170
|
>
|
|
162
171
|
‹
|
|
163
172
|
</button>
|
|
@@ -166,7 +175,7 @@ export function ConversationsInboxList({
|
|
|
166
175
|
type="button"
|
|
167
176
|
onClick={() => inbox.goToPage(inbox.page + 1)}
|
|
168
177
|
disabled={inbox.page === inbox.pageCount}
|
|
169
|
-
aria-label=
|
|
178
|
+
aria-label={PAGINATION_LABELS.next} data-cv-tooltip={PAGINATION_LABELS.next}
|
|
170
179
|
>
|
|
171
180
|
›
|
|
172
181
|
</button>
|
|
@@ -174,7 +183,7 @@ export function ConversationsInboxList({
|
|
|
174
183
|
type="button"
|
|
175
184
|
onClick={() => inbox.goToPage(inbox.pageCount)}
|
|
176
185
|
disabled={inbox.page === inbox.pageCount}
|
|
177
|
-
aria-label=
|
|
186
|
+
aria-label={PAGINATION_LABELS.last} data-cv-tooltip={PAGINATION_LABELS.last}
|
|
178
187
|
>
|
|
179
188
|
»
|
|
180
189
|
</button>
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
15
|
+
import { Check, CheckCheck, FlaskConical, Hourglass, Mail, MessagesSquare } from 'lucide-react'
|
|
15
16
|
|
|
17
|
+
import { ICON_SIZE_ACTION, ICON_SIZE_INLINE } from '../icon.constant'
|
|
16
18
|
import type { ConversationContextEntry } from '../ConversationContextPanel'
|
|
17
19
|
import type { ConversationHeaderUtility } from '../ConversationHeader'
|
|
18
20
|
import type { QuickReply } from '../MessageComposer'
|
|
@@ -23,6 +25,7 @@ import { ConversationPane } from './ConversationPane'
|
|
|
23
25
|
import { ConversationsInboxList } from './ConversationsInboxList'
|
|
24
26
|
import { DEFAULT_CONVERSATIONS_WORKSPACE_LABELS, type ConversationsWorkspaceLabels } from './labels'
|
|
25
27
|
import { useConversationsInbox, type UseConversationsInboxResult } from './useConversationsInbox'
|
|
28
|
+
import { TooltipLayer } from '../Tooltip'
|
|
26
29
|
|
|
27
30
|
export interface ConversationsWorkspaceSimulator {
|
|
28
31
|
/**
|
|
@@ -32,7 +35,8 @@ export interface ConversationsWorkspaceSimulator {
|
|
|
32
35
|
render(params: { conversationId: string; close: () => void }): ReactNode
|
|
33
36
|
/** Ausente = ligado. Serve para esconder fora de desenvolvimento sem condicionar o JSX. */
|
|
34
37
|
readonly enabled?: boolean
|
|
35
|
-
|
|
38
|
+
/** Ícone da biblioteca (lucide) no utilitário do cabeçalho. Ausente, entra o frasco de teste. */
|
|
39
|
+
readonly icon?: ReactNode
|
|
36
40
|
readonly label?: string
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -166,7 +170,7 @@ export function ConversationsWorkspace({
|
|
|
166
170
|
...fromProduct,
|
|
167
171
|
{
|
|
168
172
|
key: 'simulator',
|
|
169
|
-
icon: simulator?.icon ??
|
|
173
|
+
icon: simulator?.icon ?? <FlaskConical size={ICON_SIZE_ACTION} />,
|
|
170
174
|
label: simulator?.label ?? 'Simular cliente',
|
|
171
175
|
active: simulatorOpen,
|
|
172
176
|
run: () => setSimulatorOpen((open) => !open),
|
|
@@ -176,22 +180,23 @@ export function ConversationsWorkspace({
|
|
|
176
180
|
|
|
177
181
|
return (
|
|
178
182
|
<div className={`cv-workspace${className ? ` ${className}` : ''}`}>
|
|
183
|
+
<TooltipLayer />
|
|
179
184
|
{/* Cabeçalho denso em tela estreita: ícone + número. O rótulo escrito ocupava três linhas em
|
|
180
185
|
375px e empurrava a lista para fora da tela. */}
|
|
181
186
|
<header className="cv-workspace-header">
|
|
182
187
|
<div className="cv-workspace-header__titles">
|
|
183
188
|
<h1>{labels.title}</h1>
|
|
184
189
|
<p>
|
|
185
|
-
<span
|
|
186
|
-
|
|
190
|
+
<span data-cv-tooltip={labels.conversations} className="cv-stat">
|
|
191
|
+
<MessagesSquare size={ICON_SIZE_INLINE} aria-hidden="true" /> {inbox.totalCount}
|
|
187
192
|
<span className="cv-only-wide"> {labels.conversations}</span>
|
|
188
193
|
</span>
|
|
189
|
-
<span
|
|
190
|
-
|
|
194
|
+
<span data-cv-tooltip={labels.waiting} className="cv-stat cv-workspace-header__waiting">
|
|
195
|
+
<Hourglass size={ICON_SIZE_INLINE} aria-hidden="true" /> {inbox.waitingCount}
|
|
191
196
|
<span className="cv-only-wide"> {labels.waiting}</span>
|
|
192
197
|
</span>
|
|
193
|
-
<span
|
|
194
|
-
|
|
198
|
+
<span data-cv-tooltip={labels.unread} className="cv-stat">
|
|
199
|
+
<Mail size={ICON_SIZE_INLINE} aria-hidden="true" /> {inbox.unreadCount}
|
|
195
200
|
<span className="cv-only-wide"> {labels.unread}</span>
|
|
196
201
|
</span>
|
|
197
202
|
</p>
|
|
@@ -202,22 +207,24 @@ export function ConversationsWorkspace({
|
|
|
202
207
|
type="button"
|
|
203
208
|
onClick={() => inbox.setWaitingOnly(!inbox.waitingOnly)}
|
|
204
209
|
aria-pressed={inbox.waitingOnly}
|
|
205
|
-
|
|
210
|
+
data-cv-tooltip={labels.waitingOnly} aria-label={labels.waitingOnly}
|
|
206
211
|
className={inbox.waitingOnly ? 'cv-workspace-toggle cv-workspace-toggle--on' : 'cv-workspace-toggle'}
|
|
207
212
|
>
|
|
208
|
-
|
|
213
|
+
<Hourglass size={ICON_SIZE_ACTION} aria-hidden="true" />
|
|
214
|
+
<span className="cv-only-wide">{labels.waitingOnly}</span>
|
|
209
215
|
</button>
|
|
210
216
|
{/* O contador só aparece com seleção: " (0)" era texto morto ao lado do ícone. */}
|
|
211
217
|
<button
|
|
212
218
|
type="button"
|
|
213
219
|
onClick={() => void inbox.markSelectedAsRead()}
|
|
214
220
|
disabled={inbox.selectedIds.size === 0 || inbox.busy}
|
|
215
|
-
|
|
221
|
+
data-cv-tooltip={labels.markSelectedAsRead}
|
|
216
222
|
aria-label={labels.markSelectedAsRead}
|
|
217
223
|
className="cv-workspace-toggle"
|
|
218
224
|
>
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
<Check size={ICON_SIZE_ACTION} aria-hidden="true" />
|
|
226
|
+
<span className="cv-only-wide">{labels.markSelectedAsRead}</span>
|
|
227
|
+
{inbox.selectedIds.size > 0 ? <span>({inbox.selectedIds.size})</span> : null}
|
|
221
228
|
</button>
|
|
222
229
|
{/* Só com não lida na tela e só onde o host implementa a rota: sem isso o botão zerava
|
|
223
230
|
nada e ainda assim ocupava o cabeçalho. */}
|
|
@@ -226,10 +233,11 @@ export function ConversationsWorkspace({
|
|
|
226
233
|
type="button"
|
|
227
234
|
onClick={() => void inbox.markAllAsRead()}
|
|
228
235
|
disabled={inbox.busy}
|
|
229
|
-
|
|
236
|
+
data-cv-tooltip={labels.markAllAsRead} aria-label={labels.markAllAsRead}
|
|
230
237
|
className="cv-workspace-toggle"
|
|
231
238
|
>
|
|
232
|
-
|
|
239
|
+
<CheckCheck size={ICON_SIZE_ACTION} aria-hidden="true" />
|
|
240
|
+
<span className="cv-only-wide">{labels.markAllAsRead}</span>
|
|
233
241
|
</button>
|
|
234
242
|
) : null}
|
|
235
243
|
{renderHeaderActions?.(inbox)}
|
|
@@ -283,7 +291,7 @@ export function ConversationsWorkspace({
|
|
|
283
291
|
now={inbox.now}
|
|
284
292
|
busy={inbox.busy}
|
|
285
293
|
labels={labels}
|
|
286
|
-
{...(inbox.canTakeover ? { onTakeover: () =>
|
|
294
|
+
{...(inbox.canTakeover ? { onTakeover: () => inbox.takeover(selected.id) } : {})}
|
|
287
295
|
{...(inbox.canTakeover ? { onReturnToBot: () => void inbox.releaseToBot(selected.id) } : {})}
|
|
288
296
|
{...(inbox.canFinalize ? { onFinish: () => void inbox.finalize(selected.id) } : {})}
|
|
289
297
|
{...(flowLabelOf ? { flowLabel: flowLabelOf(selected) } : {})}
|
|
@@ -265,8 +265,11 @@ export function useConversationsInbox(params: UseConversationsInboxParams = {}):
|
|
|
265
265
|
selectedConversation: conversations.find((conversation) => conversation.id === selectedId),
|
|
266
266
|
loading,
|
|
267
267
|
now,
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
// Com paginação no servidor `conversations` é só a página, então contar o array mostrava o
|
|
269
|
+
// tamanho da página no lugar do tamanho da base. Não lidas e aguardando seguem sendo da página:
|
|
270
|
+
// somar a base inteira exige agregado do servidor, que a listagem não devolve.
|
|
271
|
+
totalCount: params.serverPaginated ? total : conversations.length,
|
|
272
|
+
unreadCount: conversations.reduce((sum, conversation) => sum + conversation.unread, 0),
|
|
270
273
|
waitingCount: conversations.filter((conversation) => conversation.waitingHuman).length,
|
|
271
274
|
filteredCount: totalForPaging,
|
|
272
275
|
expiredSelectedCount: conversations.filter(
|