@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.41
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/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
- package/dist/chunk-BJNRLLDO.js +2708 -0
- package/dist/chunk-DKPXKQGC.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +422 -5
- package/dist/flows/index.js +2389 -678
- package/dist/index.d.ts +1171 -42
- package/dist/index.js +3755 -843
- package/dist/preview/index.d.ts +157 -42
- package/dist/preview/index.js +772 -191
- package/dist/styles.css +893 -0
- package/package.json +9 -8
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +14 -3
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +347 -24
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +163 -45
- package/src/ConversationListItem.tsx +19 -2
- package/src/ConversationLocalesProvider.tsx +28 -0
- package/src/ConversationRow.tsx +31 -7
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/DocumentsLibrary.tsx +382 -0
- package/src/EmojiPicker.tsx +70 -55
- package/src/FileIcon.test.ts +83 -0
- package/src/FileIcon.tsx +88 -11
- package/src/InteractiveMessage.test.tsx +41 -0
- package/src/InteractiveMessage.tsx +146 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +92 -16
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +75 -6
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +155 -19
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- 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 +167 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +34 -7
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/audioRecorderFormat.test.ts +67 -0
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +550 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/flows/FlowConnectionEdge.tsx +104 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowLegend.tsx +125 -0
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +219 -34
- package/src/flows/FlowNodePanel.tsx +153 -39
- package/src/flows/FlowPalette.tsx +106 -69
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1219 -0
- package/src/flows/flowCanvasModel.test.ts +456 -0
- package/src/flows/flowCanvasModel.ts +378 -0
- package/src/flows/flowEditorOps.test.ts +276 -0
- package/src/flows/flowEditorOps.ts +202 -0
- package/src/flows/flowGraph.ts +78 -53
- package/src/flows/index.ts +51 -2
- package/src/flows/labels.ts +180 -0
- package/src/flows/workspaceContract.test.ts +126 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +129 -13
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +225 -17
- package/src/preview/ConversationSimulatorClient.ts +143 -0
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +131 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +175 -15
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +127 -4
- package/src/preview/index.ts +51 -3
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +288 -1
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +185 -10
- package/src/quickReply.test.ts +58 -0
- package/src/settings/MessagesWorkspace.tsx +571 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
- package/src/styles.css +858 -0
- package/src/types.ts +64 -1
- package/src/useWaitingNotifications.ts +74 -29
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +423 -0
- package/src/workspace/index.ts +17 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-4R6Y43DQ.js +0 -726
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-C0PtaO7S.d.ts +0 -207
package/src/types.ts
CHANGED
|
@@ -20,9 +20,46 @@ export interface ConversationsFeatures {
|
|
|
20
20
|
darkMode?: boolean
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Recorte do bloco `interactive` da Meta que a UI precisa para desenhar o menu. Fica solto (e não
|
|
25
|
+
* espelhando o contrato inteiro) porque o que chega do banco é o payload cru já enviado ao
|
|
26
|
+
* WhatsApp: qualquer campo que a UI não conheça é ignorado, nunca causa erro de render.
|
|
27
|
+
*/
|
|
28
|
+
export interface InteractiveOption {
|
|
29
|
+
id: string
|
|
30
|
+
title: string
|
|
31
|
+
description?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InteractiveSection {
|
|
35
|
+
title?: string
|
|
36
|
+
rows?: InteractiveOption[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface InteractivePayload {
|
|
40
|
+
type?: 'button' | 'list' | string
|
|
41
|
+
header?: { text?: string }
|
|
42
|
+
body?: { text?: string }
|
|
43
|
+
footer?: { text?: string }
|
|
44
|
+
action?: {
|
|
45
|
+
/** Rótulo do botão que abre a lista — só existe em `type: 'list'`. */
|
|
46
|
+
button?: string
|
|
47
|
+
sections?: InteractiveSection[]
|
|
48
|
+
buttons?: { reply?: InteractiveOption }[]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Como o cliente respondeu a um menu: por botão ou por item de lista. */
|
|
53
|
+
export type InteractiveSelection = {
|
|
54
|
+
readonly kind: 'button' | 'list'
|
|
55
|
+
readonly option: InteractiveOption
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
export interface MessagePayload {
|
|
24
59
|
id: string
|
|
25
|
-
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template'
|
|
60
|
+
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template' | 'interactive'
|
|
61
|
+
/** Payload cru da mensagem. Em `type: 'interactive'`, carrega o menu que o cliente vê. */
|
|
62
|
+
payload?: InteractivePayload | null
|
|
26
63
|
content?: string
|
|
27
64
|
caption?: string
|
|
28
65
|
mediaUrl?: string
|
|
@@ -47,6 +84,32 @@ export interface MessagePayload {
|
|
|
47
84
|
* diferente de avaliado e limpo.
|
|
48
85
|
*/
|
|
49
86
|
moderation?: { isOffensive: boolean; terms: string[] } | null
|
|
87
|
+
/**
|
|
88
|
+
* Transcrição do áudio, vinda do backend — a UI só exibe, nunca transcreve. Rodar STT no browser
|
|
89
|
+
* exigiria baixar modelo por aba e daria resultado diferente por versão de cliente.
|
|
90
|
+
*
|
|
91
|
+
* `null`/ausente = não avaliado, que é diferente de `'done'` com texto vazio (áudio em silêncio,
|
|
92
|
+
* já processado). É essa distinção que decide se o balão oferece "transcrever" ou "sem fala
|
|
93
|
+
* detectada".
|
|
94
|
+
*/
|
|
95
|
+
transcription?: MessageTranscription | null
|
|
50
96
|
isFirstInGroup?: boolean
|
|
51
97
|
isLastInGroup?: boolean
|
|
52
98
|
}
|
|
99
|
+
|
|
100
|
+
export type TranscriptionStatus = 'pending' | 'done' | 'failed' | 'unsupported'
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Quando transcrever, escolhido nas configurações da empresa. Espelha o
|
|
104
|
+
* `TranscriptionMode` de `@adatechnology/meta-whatsapp-contracts`; declarado aqui para o pacote de
|
|
105
|
+
* UI não obrigar quem só desenha telas a instalar os contratos do backend.
|
|
106
|
+
*/
|
|
107
|
+
export type TranscriptionMode = 'auto' | 'onDemand'
|
|
108
|
+
|
|
109
|
+
export interface MessageTranscription {
|
|
110
|
+
status: TranscriptionStatus
|
|
111
|
+
text?: string | null
|
|
112
|
+
/** ISO 639-1 ou nome do idioma, conforme o engine. Exibido como dica, não interpretado. */
|
|
113
|
+
language?: string | null
|
|
114
|
+
engine?: string | null
|
|
115
|
+
}
|
|
@@ -1,64 +1,109 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
2
|
import { useConversations } from './providers/ConversationsProvider'
|
|
3
|
-
import
|
|
3
|
+
import { conversationsOf } from './lib/paginated'
|
|
4
|
+
import type { ConversationSummary, ListConversationsParams } from './providers/types'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const DEFAULT_POLL_INTERVAL_MS = 10_000
|
|
7
|
+
const DEFAULT_LIMIT = 50
|
|
8
|
+
|
|
9
|
+
export interface UseWaitingNotificationsLabels {
|
|
10
|
+
/** Título da notificação do sistema. Recebe a conversa para o host escolher nome × número. */
|
|
11
|
+
title: (conversation: ConversationSummary) => string
|
|
12
|
+
body: (conversation: ConversationSummary) => string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseWaitingNotificationsParams {
|
|
16
|
+
/**
|
|
17
|
+
* Repassado cru ao `fetchConversations`. É o que permite filtrar não lidas **no servidor** em
|
|
18
|
+
* vez de baixar a lista inteira e contar no cliente: um painel com milhares de conversas não
|
|
19
|
+
* pode paginar 50 por vez atrás de quem tem `unread > 0`.
|
|
20
|
+
*/
|
|
21
|
+
readonly params?: ListConversationsParams
|
|
22
|
+
readonly intervalMs?: number
|
|
23
|
+
/** Desliga o polling sem desmontar quem chama — útil com a aba em segundo plano. */
|
|
24
|
+
readonly enabled?: boolean
|
|
25
|
+
readonly icon?: string
|
|
26
|
+
readonly labels?: Partial<UseWaitingNotificationsLabels>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseWaitingNotificationsResult {
|
|
6
30
|
unreadCount: number
|
|
7
31
|
conversations: ConversationSummary[]
|
|
32
|
+
/**
|
|
33
|
+
* Releitura sob demanda. Existe porque o polling é o piso, não o mecanismo: quem já recebe SSE
|
|
34
|
+
* ou acabou de marcar tudo como lido sabe da mudança antes do próximo tick, e esperar 10s para
|
|
35
|
+
* o contador acompanhar faz a interface parecer travada.
|
|
36
|
+
*/
|
|
37
|
+
refresh: () => Promise<void>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_LABELS: UseWaitingNotificationsLabels = {
|
|
41
|
+
title: (conversation) => conversation.clientName ?? conversation.whatsappNumber,
|
|
42
|
+
body: (conversation) => conversation.lastContent ?? 'Nova mensagem',
|
|
8
43
|
}
|
|
9
44
|
|
|
10
|
-
export function useWaitingNotifications(): UseWaitingNotificationsResult {
|
|
11
|
-
const
|
|
45
|
+
export function useWaitingNotifications(params?: UseWaitingNotificationsParams): UseWaitingNotificationsResult {
|
|
46
|
+
const conversationsContext = useConversations()
|
|
12
47
|
const [conversations, setConversations] = useState<ConversationSummary[]>([])
|
|
13
48
|
const previousUnreadMap = useRef<Map<string, number>>(new Map())
|
|
14
49
|
const notifiedIds = useRef<Set<string>>(new Set())
|
|
50
|
+
// Trava de reentrância: com a rede lenta, o tick de 10s dispara sobre a busca anterior ainda em
|
|
51
|
+
// voo, e duas respostas fora de ordem fazem o contador oscilar e a notificação repetir.
|
|
52
|
+
const isPollingRef = useRef(false)
|
|
53
|
+
|
|
54
|
+
const isEnabled = params?.enabled ?? true
|
|
55
|
+
const intervalMs = params?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS
|
|
56
|
+
const icon = params?.icon
|
|
57
|
+
const listParams = params?.params
|
|
58
|
+
const labelTitle = params?.labels?.title ?? DEFAULT_LABELS.title
|
|
59
|
+
const labelBody = params?.labels?.body ?? DEFAULT_LABELS.body
|
|
15
60
|
|
|
16
|
-
const
|
|
17
|
-
if (!
|
|
61
|
+
const refresh = useCallback(async () => {
|
|
62
|
+
if (!conversationsContext || isPollingRef.current) return
|
|
63
|
+
isPollingRef.current = true
|
|
18
64
|
|
|
19
65
|
try {
|
|
20
|
-
const result =
|
|
66
|
+
const result = conversationsOf(
|
|
67
|
+
await conversationsContext.api.fetchConversations({ limit: DEFAULT_LIMIT, ...listParams }),
|
|
68
|
+
)
|
|
21
69
|
setConversations(result)
|
|
22
70
|
|
|
23
71
|
const currentMap = new Map<string, number>()
|
|
24
|
-
for (const
|
|
25
|
-
currentMap.set(conv.id, conv.unread)
|
|
26
|
-
}
|
|
72
|
+
for (const conversation of result) currentMap.set(conversation.id, conversation.unread)
|
|
27
73
|
|
|
28
|
-
for (const
|
|
29
|
-
if (
|
|
74
|
+
for (const conversation of result) {
|
|
75
|
+
if (conversation.unread <= 0) continue
|
|
30
76
|
|
|
31
|
-
const
|
|
77
|
+
const previousUnread = previousUnreadMap.current.get(conversation.id) ?? 0
|
|
78
|
+
if (conversation.unread <= previousUnread || notifiedIds.current.has(conversation.id)) continue
|
|
32
79
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (typeof window !== 'undefined' && 'Notification' in window && Notification.permission === 'granted') {
|
|
37
|
-
const title = conv.clientName ?? conv.whatsappNumber
|
|
38
|
-
const body = conv.lastContent ?? 'Nova mensagem'
|
|
39
|
-
new Notification(title, { body, icon: '/favicon.ico' })
|
|
40
|
-
}
|
|
80
|
+
notifiedIds.current.add(conversation.id)
|
|
81
|
+
if (typeof window !== 'undefined' && 'Notification' in window && Notification.permission === 'granted') {
|
|
82
|
+
new Notification(labelTitle(conversation), { body: labelBody(conversation), ...(icon ? { icon } : {}) })
|
|
41
83
|
}
|
|
42
84
|
}
|
|
43
85
|
|
|
44
86
|
previousUnreadMap.current = currentMap
|
|
45
87
|
} catch {
|
|
46
|
-
//
|
|
88
|
+
// Contador de badge não interrompe o atendimento: uma falha de rede some no próximo tick.
|
|
89
|
+
} finally {
|
|
90
|
+
isPollingRef.current = false
|
|
47
91
|
}
|
|
48
|
-
}, [
|
|
92
|
+
}, [conversationsContext, listParams, icon, labelTitle, labelBody])
|
|
49
93
|
|
|
50
94
|
useEffect(() => {
|
|
95
|
+
if (!isEnabled) return
|
|
96
|
+
|
|
51
97
|
if (typeof window !== 'undefined' && 'Notification' in window && Notification.permission === 'default') {
|
|
52
98
|
Notification.requestPermission()
|
|
53
99
|
}
|
|
54
100
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const interval = setInterval(pollConversations, 10_000)
|
|
101
|
+
void refresh()
|
|
102
|
+
const interval = setInterval(() => void refresh(), intervalMs)
|
|
58
103
|
return () => clearInterval(interval)
|
|
59
|
-
}, [
|
|
104
|
+
}, [refresh, isEnabled, intervalMs])
|
|
60
105
|
|
|
61
|
-
const unreadCount = conversations.reduce((sum,
|
|
106
|
+
const unreadCount = conversations.reduce((sum, conversation) => sum + conversation.unread, 0)
|
|
62
107
|
|
|
63
|
-
return { unreadCount, conversations }
|
|
108
|
+
return { unreadCount, conversations, refresh }
|
|
64
109
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escolha do template para o envio em lote.
|
|
3
|
+
*
|
|
4
|
+
* Fora da janela de 24h só template abre conversa, e a lista aprovada muda por conta da Meta — daí
|
|
5
|
+
* a busca: quem tem trinta templates não acha o certo rolando. Só aparece quando o host implementa
|
|
6
|
+
* `listTemplates`; sem a listagem não há o que escolher, e o envio cai no template padrão.
|
|
7
|
+
*
|
|
8
|
+
* O aviso de "nenhuma selecionada está fora da janela" existe porque o envio parecia falhar em
|
|
9
|
+
* silêncio: dentro da janela a Meta recusa o template, e a tela não dizia por quê.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
import { useConversations } from '../providers/ConversationsProvider'
|
|
15
|
+
import type { ConversationTemplate } from '../providers/types'
|
|
16
|
+
import type { ConversationsWorkspaceLabels } from './labels'
|
|
17
|
+
|
|
18
|
+
export interface BulkTemplateModalProps {
|
|
19
|
+
readonly labels: ConversationsWorkspaceLabels
|
|
20
|
+
/** Quantas das selecionadas estão fora da janela — as únicas que o template atinge. */
|
|
21
|
+
readonly expiredCount: number
|
|
22
|
+
readonly sending: boolean
|
|
23
|
+
readonly onClose: () => void
|
|
24
|
+
readonly onSend: (templateName: string | undefined) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function BulkTemplateModal({
|
|
28
|
+
labels,
|
|
29
|
+
expiredCount,
|
|
30
|
+
sending,
|
|
31
|
+
onClose,
|
|
32
|
+
onSend,
|
|
33
|
+
}: BulkTemplateModalProps) {
|
|
34
|
+
const context = useConversations()
|
|
35
|
+
if (!context) {
|
|
36
|
+
throw new Error('BulkTemplateModal requires an ancestor <ConversationsProvider>')
|
|
37
|
+
}
|
|
38
|
+
const { api } = context
|
|
39
|
+
|
|
40
|
+
const [templates, setTemplates] = useState<readonly ConversationTemplate[]>([])
|
|
41
|
+
const [search, setSearch] = useState('')
|
|
42
|
+
const [selected, setSelected] = useState('')
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let active = true
|
|
46
|
+
void api
|
|
47
|
+
.listTemplates?.()
|
|
48
|
+
.then((loaded) => {
|
|
49
|
+
if (active) setTemplates(loaded)
|
|
50
|
+
})
|
|
51
|
+
.catch(() => {
|
|
52
|
+
// Lista vazia já comunica o que houve; um alerta a mais competiria com o atendimento.
|
|
53
|
+
})
|
|
54
|
+
return () => {
|
|
55
|
+
active = false
|
|
56
|
+
}
|
|
57
|
+
}, [api])
|
|
58
|
+
|
|
59
|
+
const filtered = useMemo(() => {
|
|
60
|
+
const term = search.trim().toLowerCase()
|
|
61
|
+
if (!term) return templates
|
|
62
|
+
return templates.filter((template) => template.name.toLowerCase().includes(term))
|
|
63
|
+
}, [templates, search])
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="cv-workspace-modal" role="dialog" aria-modal="true" aria-label={labels.templateModalTitle}>
|
|
67
|
+
<div className="cv-workspace-modal__box">
|
|
68
|
+
<header className="cv-workspace-modal__header">
|
|
69
|
+
<div>
|
|
70
|
+
<h3>{labels.templateModalTitle}</h3>
|
|
71
|
+
<p>{labels.templateModalAvailable(templates.length)}</p>
|
|
72
|
+
</div>
|
|
73
|
+
<button data-cv-tooltip={labels.templateModalCancel} type="button" onClick={onClose} aria-label={labels.templateModalCancel}>
|
|
74
|
+
✕
|
|
75
|
+
</button>
|
|
76
|
+
</header>
|
|
77
|
+
|
|
78
|
+
<div className="cv-workspace-modal__search">
|
|
79
|
+
<input
|
|
80
|
+
type="search"
|
|
81
|
+
value={search}
|
|
82
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
83
|
+
placeholder={labels.templateModalSearch}
|
|
84
|
+
autoFocus
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="cv-workspace-modal__list">
|
|
89
|
+
{filtered.length === 0 ? <p className="cv-workspace-empty">{labels.templateModalEmpty}</p> : null}
|
|
90
|
+
{filtered.map((template) => (
|
|
91
|
+
<button
|
|
92
|
+
data-cv-tooltip={template.name} aria-label={template.name}
|
|
93
|
+
key={template.name}
|
|
94
|
+
type="button"
|
|
95
|
+
// Reclicar desmarca: sem isso não havia como voltar ao template padrão depois de
|
|
96
|
+
// escolher um por engano.
|
|
97
|
+
onClick={() => setSelected(template.name === selected ? '' : template.name)}
|
|
98
|
+
aria-pressed={template.name === selected}
|
|
99
|
+
className={`cv-workspace-modal__item${
|
|
100
|
+
template.name === selected ? ' cv-workspace-modal__item--on' : ''
|
|
101
|
+
}`}
|
|
102
|
+
>
|
|
103
|
+
<span className="cv-workspace-modal__item-name">{template.name}</span>
|
|
104
|
+
<span className="cv-workspace-modal__item-meta">
|
|
105
|
+
{template.language}
|
|
106
|
+
{template.category ? ` · ${template.category.toLowerCase()}` : ''}
|
|
107
|
+
</span>
|
|
108
|
+
</button>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{expiredCount === 0 ? (
|
|
113
|
+
<p className="cv-workspace-modal__warning">{labels.templateModalNoneExpired}</p>
|
|
114
|
+
) : null}
|
|
115
|
+
|
|
116
|
+
<footer className="cv-workspace-modal__footer">
|
|
117
|
+
<button data-cv-tooltip={labels.templateModalCancel} aria-label={labels.templateModalCancel} type="button" onClick={onClose}>
|
|
118
|
+
{labels.templateModalCancel}
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
data-cv-tooltip={sending ? labels.templateModalSending : labels.templateModalSend(expiredCount)} aria-label={sending ? labels.templateModalSending : labels.templateModalSend(expiredCount)}
|
|
122
|
+
type="button"
|
|
123
|
+
disabled={sending || expiredCount === 0}
|
|
124
|
+
onClick={() => onSend(selected || undefined)}
|
|
125
|
+
>
|
|
126
|
+
{sending ? labels.templateModalSending : labels.templateModalSend(expiredCount)}
|
|
127
|
+
</button>
|
|
128
|
+
</footer>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|