@adatechnology/conversations-ui 0.1.0-rc.26 → 0.1.0-rc.27
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 +2 -0
- package/dist/flows/index.js +61 -18
- 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 +1 -1
- 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/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 +13 -4
- package/src/flows/labels.ts +4 -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
package/src/Tooltip.tsx
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camada única de dicas do pacote.
|
|
3
|
+
*
|
|
4
|
+
* O `title` nativo existia em todos os botões e mesmo assim a dica "não aparecia": o navegador
|
|
5
|
+
* espera de 1 a 2 segundos antes de desenhar, o balão sai fora do tema e some ao menor movimento do
|
|
6
|
+
* mouse. Aqui a dica é do produto — aparece em 120ms, com o mesmo contraste do resto da interface.
|
|
7
|
+
*
|
|
8
|
+
* Um só ouvinte no documento atende a interface inteira: cada botão declara `data-cv-tooltip` e não
|
|
9
|
+
* precisa de estado, ref ou wrapper próprio. O balão é impresso em `document.body` via portal
|
|
10
|
+
* porque `overflow` de container (a régua de respostas rápidas rola na horizontal) recortaria um
|
|
11
|
+
* balão desenhado dentro do próprio botão.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useRef, useState } from 'react'
|
|
15
|
+
import { createPortal } from 'react-dom'
|
|
16
|
+
|
|
17
|
+
/** Atributo que marca um elemento como portador de dica. */
|
|
18
|
+
export const TOOLTIP_ATTRIBUTE = 'data-cv-tooltip'
|
|
19
|
+
|
|
20
|
+
const TOOLTIP_DELAY_MS = 120
|
|
21
|
+
const TOOLTIP_GAP_PX = 8
|
|
22
|
+
/** Margem para o balão não colar na borda da janela quando o alvo está no canto. */
|
|
23
|
+
const TOOLTIP_EDGE_PX = 72
|
|
24
|
+
|
|
25
|
+
export type TooltipPlacement = 'top' | 'bottom'
|
|
26
|
+
|
|
27
|
+
export interface TooltipPositionParams {
|
|
28
|
+
readonly targetRect: { top: number; bottom: number; left: number; width: number }
|
|
29
|
+
readonly viewportWidth: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TooltipPosition {
|
|
33
|
+
left: number
|
|
34
|
+
top: number
|
|
35
|
+
placement: TooltipPlacement
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface TooltipState extends TooltipPosition {
|
|
39
|
+
text: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Separado do componente por ser a única parte com regra: o resto é ouvir evento e desenhar. */
|
|
43
|
+
export function tooltipPositionOf({ targetRect, viewportWidth }: TooltipPositionParams): TooltipPosition {
|
|
44
|
+
// Sem espaço acima (botão no topo da tela), o balão desce em vez de sair da janela.
|
|
45
|
+
const placement: TooltipPlacement = targetRect.top < TOOLTIP_EDGE_PX ? 'bottom' : 'top'
|
|
46
|
+
const centerX = targetRect.left + targetRect.width / 2
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
left: Math.min(Math.max(centerX, TOOLTIP_EDGE_PX), viewportWidth - TOOLTIP_EDGE_PX),
|
|
50
|
+
top: placement === 'top' ? targetRect.top - TOOLTIP_GAP_PX : targetRect.bottom + TOOLTIP_GAP_PX,
|
|
51
|
+
placement,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tooltipStateFor(target: HTMLElement, text: string): TooltipState {
|
|
56
|
+
const position = tooltipPositionOf({
|
|
57
|
+
targetRect: target.getBoundingClientRect(),
|
|
58
|
+
viewportWidth: window.innerWidth,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return { text, ...position }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Só uma camada desenha por vez. A camada é montada por cada superfície do pacote (inbox, fluxos,
|
|
66
|
+
* documentos) para o host não precisar montá-la à mão; duas superfícies na mesma tela renderizariam
|
|
67
|
+
* dois balões sobrepostos sem essa trava.
|
|
68
|
+
*/
|
|
69
|
+
let layerOwner: object | undefined
|
|
70
|
+
|
|
71
|
+
export function TooltipLayer() {
|
|
72
|
+
const identity = useRef({})
|
|
73
|
+
const [isOwner, setIsOwner] = useState(false)
|
|
74
|
+
const [tooltip, setTooltip] = useState<TooltipState | undefined>(undefined)
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const owned = identity.current
|
|
78
|
+
if (layerOwner && layerOwner !== owned) return undefined
|
|
79
|
+
layerOwner = owned
|
|
80
|
+
setIsOwner(true)
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
if (layerOwner === owned) layerOwner = undefined
|
|
84
|
+
}
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!isOwner) return undefined
|
|
89
|
+
|
|
90
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
91
|
+
|
|
92
|
+
function cancelPending() {
|
|
93
|
+
if (timer) clearTimeout(timer)
|
|
94
|
+
timer = undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hide() {
|
|
98
|
+
cancelPending()
|
|
99
|
+
setTooltip(undefined)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleEnter(event: Event) {
|
|
103
|
+
const origin = event.target
|
|
104
|
+
if (!(origin instanceof Element)) return
|
|
105
|
+
const target = origin.closest(`[${TOOLTIP_ATTRIBUTE}]`)
|
|
106
|
+
if (!(target instanceof HTMLElement)) {
|
|
107
|
+
hide()
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
const text = target.getAttribute(TOOLTIP_ATTRIBUTE)
|
|
111
|
+
if (!text) {
|
|
112
|
+
hide()
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
cancelPending()
|
|
116
|
+
timer = setTimeout(() => setTooltip(tooltipStateFor(target, text)), TOOLTIP_DELAY_MS)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Sair da janela não dispara `pointerover` em elemento nenhum: sem isto, o balão ficaria aceso
|
|
120
|
+
// depois que o ponteiro já saiu da página.
|
|
121
|
+
function handleLeaveWindow(event: PointerEvent) {
|
|
122
|
+
if (!event.relatedTarget) hide()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
126
|
+
if (event.key === 'Escape') hide()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
document.addEventListener('pointerover', handleEnter)
|
|
130
|
+
document.addEventListener('pointerout', handleLeaveWindow)
|
|
131
|
+
document.addEventListener('focusin', handleEnter)
|
|
132
|
+
document.addEventListener('pointerdown', hide)
|
|
133
|
+
document.addEventListener('focusout', hide)
|
|
134
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
135
|
+
// Captura: rolagem em qualquer container interno invalida a posição já medida.
|
|
136
|
+
window.addEventListener('scroll', hide, true)
|
|
137
|
+
window.addEventListener('resize', hide)
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
cancelPending()
|
|
141
|
+
document.removeEventListener('pointerover', handleEnter)
|
|
142
|
+
document.removeEventListener('pointerout', handleLeaveWindow)
|
|
143
|
+
document.removeEventListener('focusin', handleEnter)
|
|
144
|
+
document.removeEventListener('pointerdown', hide)
|
|
145
|
+
document.removeEventListener('focusout', hide)
|
|
146
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
147
|
+
window.removeEventListener('scroll', hide, true)
|
|
148
|
+
window.removeEventListener('resize', hide)
|
|
149
|
+
}
|
|
150
|
+
}, [isOwner])
|
|
151
|
+
|
|
152
|
+
if (!tooltip) return null
|
|
153
|
+
|
|
154
|
+
return createPortal(
|
|
155
|
+
<div
|
|
156
|
+
role="tooltip"
|
|
157
|
+
className={`cv-tooltip cv-tooltip--${tooltip.placement}`}
|
|
158
|
+
style={{ left: tooltip.left, top: tooltip.top }}
|
|
159
|
+
>
|
|
160
|
+
{tooltip.text}
|
|
161
|
+
</div>,
|
|
162
|
+
document.body,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -117,13 +117,13 @@ export function WhatsAppMessageEditor({
|
|
|
117
117
|
return (
|
|
118
118
|
<div>
|
|
119
119
|
<div className="flex flex-wrap items-center gap-1.5 mb-2">
|
|
120
|
-
<button type="button" onClick={() => wrapSelection('*')} className={toolbarButtonClass}
|
|
120
|
+
<button type="button" onClick={() => wrapSelection('*')} className={toolbarButtonClass} data-cv-tooltip={editorLabels.boldHint} aria-label={editorLabels.bold}>
|
|
121
121
|
<Bold size={15} />
|
|
122
122
|
</button>
|
|
123
|
-
<button type="button" onClick={() => wrapSelection('_')} className={toolbarButtonClass}
|
|
123
|
+
<button type="button" onClick={() => wrapSelection('_')} className={toolbarButtonClass} data-cv-tooltip={editorLabels.italicHint} aria-label={editorLabels.italic}>
|
|
124
124
|
<Italic size={15} />
|
|
125
125
|
</button>
|
|
126
|
-
<button type="button" onClick={() => wrapSelection('~')} className={toolbarButtonClass}
|
|
126
|
+
<button type="button" onClick={() => wrapSelection('~')} className={toolbarButtonClass} data-cv-tooltip={editorLabels.strikethroughHint} aria-label={editorLabels.strikethrough}>
|
|
127
127
|
<Strikethrough size={15} />
|
|
128
128
|
</button>
|
|
129
129
|
{placeholders.length > 0 && (
|
|
@@ -135,7 +135,7 @@ export function WhatsAppMessageEditor({
|
|
|
135
135
|
type="button"
|
|
136
136
|
onClick={() => insertAtCursor(token)}
|
|
137
137
|
className="inline-flex items-center h-8 px-2 rounded-lg border border-gray-200 dark:border-gray-600 text-xs text-blue-600 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/40 transition-colors"
|
|
138
|
-
|
|
138
|
+
data-cv-tooltip={editorLabels.insertPlaceholder(token)} aria-label={editorLabels.insertPlaceholder(token)}
|
|
139
139
|
>
|
|
140
140
|
{token}
|
|
141
141
|
</button>
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* cumpre. O caminho que resta — template — fica visível na mesma altura da tela.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { AlarmClock, Send } from 'lucide-react'
|
|
10
|
+
|
|
9
11
|
import { CONVERSATION_WINDOW, type ConversationWindow } from './conversationWindow'
|
|
12
|
+
import { ICON_SIZE_ACTION, ICON_SIZE_INLINE } from './icon.constant'
|
|
10
13
|
import { cn } from './lib/cn'
|
|
11
14
|
|
|
12
15
|
export interface WindowExpiredNoticeLabels {
|
|
@@ -15,10 +18,11 @@ export interface WindowExpiredNoticeLabels {
|
|
|
15
18
|
sendTemplate: string
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
// Sem emoji no rótulo: o ícone vem ao lado, da mesma biblioteca do resto da tela.
|
|
18
22
|
export const DEFAULT_WINDOW_EXPIRED_LABELS: WindowExpiredNoticeLabels = {
|
|
19
|
-
title: '
|
|
23
|
+
title: 'Janela de 24h expirada — o WhatsApp não aceita mensagens livres.',
|
|
20
24
|
description: 'Envie uma mensagem de template para reabrir a conversa com o cliente.',
|
|
21
|
-
sendTemplate: '
|
|
25
|
+
sendTemplate: 'Enviar Template (HSM)',
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export interface WindowExpiredNoticeProps {
|
|
@@ -38,10 +42,14 @@ export function WindowExpiredNotice({
|
|
|
38
42
|
|
|
39
43
|
return (
|
|
40
44
|
<div role="status" className={cn('border-t bg-yellow-50 px-4 py-3 text-sm dark:bg-yellow-950', className)}>
|
|
41
|
-
<p className="font-medium text-yellow-900 dark:text-yellow-200">
|
|
45
|
+
<p className="flex items-center gap-2 font-medium text-yellow-900 dark:text-yellow-200">
|
|
46
|
+
<AlarmClock size={ICON_SIZE_INLINE} aria-hidden="true" className="shrink-0" />
|
|
47
|
+
{labels.title}
|
|
48
|
+
</p>
|
|
42
49
|
<p className="text-yellow-800 dark:text-yellow-300">{labels.description}</p>
|
|
43
50
|
{onSendTemplate ? (
|
|
44
|
-
<button type="button" onClick={onSendTemplate} disabled={disabled} className="cv-header-action mt-2">
|
|
51
|
+
<button data-cv-tooltip={labels.sendTemplate} aria-label={labels.sendTemplate} type="button" onClick={onSendTemplate} disabled={disabled} className="cv-header-action mt-2">
|
|
52
|
+
<Send size={ICON_SIZE_ACTION} aria-hidden="true" />
|
|
45
53
|
{labels.sendTemplate}
|
|
46
54
|
</button>
|
|
47
55
|
) : null}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aparência da barra de composição, compartilhada pelo `MessageComposer` e pelo
|
|
3
|
+
* `RichMessageComposer`. São a mesma superfície na tela: divergir aqui é o que fazia a barra rica
|
|
4
|
+
* aparecer branca sobre o fundo branco da página, com os atalhos flutuando sem faixa própria.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Faixa cinza de largura cheia; o campo de texto dentro dela é que arredonda — ordem do WhatsApp. */
|
|
8
|
+
export const COMPOSER_BAR_CLASS = 'cv-composer-bar'
|
|
9
|
+
|
|
10
|
+
/** Botão redondo da barra: formatação, emoji, variáveis e anexo têm o mesmo peso visual. */
|
|
11
|
+
export const COMPOSER_TOOL_BUTTON_CLASS =
|
|
12
|
+
'flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full transition-colors hover:bg-teal-50 hover:text-teal-600 dark:hover:bg-teal-900/30 dark:hover:text-teal-400'
|
|
13
|
+
|
|
14
|
+
/** Formatação em vigor no cursor. Sem isto o operador não sabe se o próximo caractere sai em negrito. */
|
|
15
|
+
export const COMPOSER_TOOL_BUTTON_ACTIVE_CLASS = 'bg-teal-50 text-teal-600 dark:bg-teal-900/40 dark:text-teal-400'
|
|
16
|
+
|
|
17
|
+
export const COMPOSER_TOOL_BUTTON_IDLE_CLASS = 'text-gray-400 dark:text-gray-500'
|
|
18
|
+
|
|
19
|
+
/** Marcação do monoespaçado dentro do campo — a mesma que o `waToHTML` escreve, para o trecho não
|
|
20
|
+
* mudar de cara ao recarregar um rascunho. */
|
|
21
|
+
export const COMPOSER_MONOSPACE_CLASS = 'bg-black/5 dark:bg-white/10 rounded px-0.5 font-mono text-sm'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Abaixo desta largura de barra (em px) os quatro botões de formatação recolhem num único botão com
|
|
25
|
+
* popover. Com todos abertos, sobram ~200px de barra para os oito controles e o campo de texto fica
|
|
26
|
+
* mais estreito que o próprio placeholder. É a largura da barra, não da janela: quem estreita a
|
|
27
|
+
* coluna é a prévia do simulador ao lado, com a janela intacta.
|
|
28
|
+
*/
|
|
29
|
+
export const COMPOSER_COMPACT_WIDTH = 480
|
|
30
|
+
|
|
31
|
+
/** Atalho de resposta rápida: pílula branca, que só tem contraste porque a faixa é cinza. */
|
|
32
|
+
export const QUICK_REPLY_PILL_CLASS =
|
|
33
|
+
'whitespace-nowrap rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs text-gray-600 shadow-sm transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-teal-800 dark:hover:bg-teal-950/40 dark:hover:text-teal-400'
|
|
@@ -22,12 +22,14 @@ export const CONVERSATION_WINDOW = {
|
|
|
22
22
|
} as const
|
|
23
23
|
export type ConversationWindow = (typeof CONVERSATION_WINDOW)[keyof typeof CONVERSATION_WINDOW]
|
|
24
24
|
|
|
25
|
+
// O tom pinta o próprio chip, em vez de uma bolinha ao lado do rótulo: a bolinha somava ~14px por
|
|
26
|
+
// filtro e era o que fazia a fila de cinco quebrar em duas linhas na coluna da lista.
|
|
25
27
|
export const WINDOW_FILTERS = [
|
|
26
|
-
{ value: CONVERSATION_WINDOW.ALL, label: 'Todas',
|
|
27
|
-
{ value: CONVERSATION_WINDOW.FRESH, label: '<12h',
|
|
28
|
-
{ value: CONVERSATION_WINDOW.WARNING, label: '12-21h',
|
|
29
|
-
{ value: CONVERSATION_WINDOW.CRITICAL, label: '21-24h',
|
|
30
|
-
{ value: CONVERSATION_WINDOW.EXPIRED, label: '>24h',
|
|
28
|
+
{ value: CONVERSATION_WINDOW.ALL, label: 'Todas', tone: '' },
|
|
29
|
+
{ value: CONVERSATION_WINDOW.FRESH, label: '<12h', tone: 'fresh' },
|
|
30
|
+
{ value: CONVERSATION_WINDOW.WARNING, label: '12-21h', tone: 'warning' },
|
|
31
|
+
{ value: CONVERSATION_WINDOW.CRITICAL, label: '21-24h', tone: 'critical' },
|
|
32
|
+
{ value: CONVERSATION_WINDOW.EXPIRED, label: '>24h', tone: 'expired' },
|
|
31
33
|
] as const
|
|
32
34
|
|
|
33
35
|
// Proporções da janela: 12h e 21h numa janela de 24h, mas expressas como fração para acompanharem
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { useDebouncedValue, useUrlArrayState, useUrlNumberState, useUrlStringState } from '../hooks/useUrlFilterState'
|
|
29
29
|
import type { CompanyDocument } from '../providers/types'
|
|
30
30
|
import { DEFAULT_DOCUMENTS_WORKSPACE_LABELS, type DocumentsWorkspaceLabels } from './labels'
|
|
31
|
+
import { TooltipLayer } from '../Tooltip'
|
|
31
32
|
|
|
32
33
|
const DEFAULT_PER_PAGE = 20
|
|
33
34
|
const DEFAULT_PER_PAGE_OPTIONS = [10, 20, 50, 100] as const
|
|
@@ -249,6 +250,7 @@ export function DocumentsWorkspace({
|
|
|
249
250
|
|
|
250
251
|
return (
|
|
251
252
|
<div className={cn('space-y-4', classNames?.root, className)}>
|
|
253
|
+
<TooltipLayer />
|
|
252
254
|
<header className={cn('space-y-0.5', classNames?.header)}>
|
|
253
255
|
<h2 className="text-lg font-semibold">{labels.title}</h2>
|
|
254
256
|
<p className="text-sm text-gray-500 dark:text-gray-400">{labels.subtitle(total)}</p>
|
|
@@ -297,7 +299,7 @@ export function DocumentsWorkspace({
|
|
|
297
299
|
{renderFilters?.({ extra, setExtra })}
|
|
298
300
|
|
|
299
301
|
{hasFilters ? (
|
|
300
|
-
<button type="button" onClick={clearFilters} className="cv-header-action inline-flex items-center gap-1">
|
|
302
|
+
<button data-cv-tooltip={labels.clearFilters} aria-label={labels.clearFilters} type="button" onClick={clearFilters} className="cv-header-action inline-flex items-center gap-1">
|
|
301
303
|
<X size={12} aria-hidden="true" />
|
|
302
304
|
{labels.clearFilters}
|
|
303
305
|
</button>
|
|
@@ -313,6 +315,7 @@ export function DocumentsWorkspace({
|
|
|
313
315
|
>
|
|
314
316
|
{downloadArchive ? (
|
|
315
317
|
<button
|
|
318
|
+
data-cv-tooltip={labels.bulkDownloadZip} aria-label={labels.bulkDownloadZip}
|
|
316
319
|
type="button"
|
|
317
320
|
onClick={() => void handleDownloadArchive()}
|
|
318
321
|
disabled={busy}
|
|
@@ -324,6 +327,7 @@ export function DocumentsWorkspace({
|
|
|
324
327
|
) : null}
|
|
325
328
|
{removeDocument ? (
|
|
326
329
|
<button
|
|
330
|
+
data-cv-tooltip={labels.bulkRemove(selectedIds.size)} aria-label={labels.bulkRemove(selectedIds.size)}
|
|
327
331
|
type="button"
|
|
328
332
|
onClick={() => {
|
|
329
333
|
if (window.confirm(labels.bulkRemoveConfirm(selectedIds.size))) void handleRemove(Array.from(selectedIds))
|
|
@@ -389,7 +393,7 @@ export function DocumentsWorkspace({
|
|
|
389
393
|
<td className="max-w-xs px-3 py-2">
|
|
390
394
|
<div className="flex min-w-0 items-center gap-2">
|
|
391
395
|
<FileIcon filename={document.filename} mimeType={document.mimeType} />
|
|
392
|
-
<span className="truncate font-medium"
|
|
396
|
+
<span className="truncate font-medium" data-cv-tooltip={document.filename}>
|
|
393
397
|
{document.filename}
|
|
394
398
|
</span>
|
|
395
399
|
</div>
|
|
@@ -402,7 +406,7 @@ export function DocumentsWorkspace({
|
|
|
402
406
|
<button
|
|
403
407
|
type="button"
|
|
404
408
|
onClick={() => onOpenConversation(document.conversationId)}
|
|
405
|
-
|
|
409
|
+
data-cv-tooltip={labels.openConversation} aria-label={labels.openConversation}
|
|
406
410
|
className="cv-header-action inline-flex items-center gap-1"
|
|
407
411
|
>
|
|
408
412
|
<MessageSquare size={12} aria-hidden="true" />
|
|
@@ -425,7 +429,7 @@ export function DocumentsWorkspace({
|
|
|
425
429
|
<button
|
|
426
430
|
type="button"
|
|
427
431
|
onClick={() => void handleOpen(document.id, 'inline')}
|
|
428
|
-
|
|
432
|
+
data-cv-tooltip={labels.view}
|
|
429
433
|
aria-label={`${labels.view}: ${document.filename}`}
|
|
430
434
|
className="cv-header-icon"
|
|
431
435
|
>
|
|
@@ -434,7 +438,7 @@ export function DocumentsWorkspace({
|
|
|
434
438
|
<button
|
|
435
439
|
type="button"
|
|
436
440
|
onClick={() => void handleOpen(document.id, 'attachment')}
|
|
437
|
-
|
|
441
|
+
data-cv-tooltip={labels.download}
|
|
438
442
|
aria-label={`${labels.download}: ${document.filename}`}
|
|
439
443
|
className="cv-header-icon"
|
|
440
444
|
>
|
|
@@ -447,7 +451,7 @@ export function DocumentsWorkspace({
|
|
|
447
451
|
if (window.confirm(labels.removeConfirm(document.filename))) void handleRemove([document.id])
|
|
448
452
|
}}
|
|
449
453
|
disabled={busy}
|
|
450
|
-
|
|
454
|
+
data-cv-tooltip={labels.remove}
|
|
451
455
|
aria-label={`${labels.remove}: ${document.filename}`}
|
|
452
456
|
className="cv-header-icon disabled:opacity-40"
|
|
453
457
|
>
|
|
@@ -21,7 +21,7 @@ export function FlowGroupHeader({ data }: NodeProps) {
|
|
|
21
21
|
<button
|
|
22
22
|
type="button"
|
|
23
23
|
onClick={onFocus}
|
|
24
|
-
|
|
24
|
+
data-cv-tooltip={labels.flowGroup.focus} aria-label={labels.flowGroup.focus}
|
|
25
25
|
className="hover:text-blue-600 dark:hover:text-blue-400"
|
|
26
26
|
>
|
|
27
27
|
<Maximize2 size={12} />
|
|
@@ -29,7 +29,7 @@ export function FlowGroupHeader({ data }: NodeProps) {
|
|
|
29
29
|
<button
|
|
30
30
|
type="button"
|
|
31
31
|
onClick={onClose}
|
|
32
|
-
|
|
32
|
+
data-cv-tooltip={labels.flowGroup.close} aria-label={labels.flowGroup.close}
|
|
33
33
|
className="hover:text-red-600 dark:hover:text-red-400"
|
|
34
34
|
>
|
|
35
35
|
<X size={12} />
|
|
@@ -22,11 +22,12 @@ export function FlowMapNode({ data }: NodeProps) {
|
|
|
22
22
|
<GitBranch size={14} />
|
|
23
23
|
<span className="text-sm font-semibold truncate">{label}</span>
|
|
24
24
|
{isRoot && (
|
|
25
|
-
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0"
|
|
25
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0" data-cv-tooltip={labels.startNodeTooltip} />
|
|
26
26
|
)}
|
|
27
27
|
</div>
|
|
28
28
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{labels.flowMap.nodeCount(nodeCount)}</p>
|
|
29
29
|
<button
|
|
30
|
+
data-cv-tooltip={labels.flowMap.openFlow} aria-label={labels.flowMap.openFlow}
|
|
30
31
|
type="button"
|
|
31
32
|
onClick={onOpen}
|
|
32
33
|
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline"
|
|
@@ -135,7 +135,7 @@ export function FlowNodeCard({ data }: NodeProps) {
|
|
|
135
135
|
|
|
136
136
|
return (
|
|
137
137
|
<div
|
|
138
|
-
|
|
138
|
+
data-cv-tooltip={isDetached ? labels.detachedNodeTooltip : label}
|
|
139
139
|
className={`relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? 'ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900' : ''} ${isDetached ? 'border-dashed !border-amber-400 animate-pulse' : ''}`}
|
|
140
140
|
onClick={() => onSelect(node.id)}
|
|
141
141
|
>
|
|
@@ -144,7 +144,7 @@ export function FlowNodeCard({ data }: NodeProps) {
|
|
|
144
144
|
<div className="flex items-center justify-between gap-2">
|
|
145
145
|
<span className="flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs">
|
|
146
146
|
<Icon size={12} strokeWidth={2.5} />
|
|
147
|
-
{isStart && <span className="h-1.5 w-1.5 rounded-full bg-emerald-500"
|
|
147
|
+
{isStart && <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" data-cv-tooltip={labels.startNodeTooltip} />}
|
|
148
148
|
{labels.legend[node.type]}
|
|
149
149
|
</span>
|
|
150
150
|
<div className="flex items-center gap-1">
|
|
@@ -152,7 +152,7 @@ export function FlowNodeCard({ data }: NodeProps) {
|
|
|
152
152
|
{hasWarning && <AlertTriangle size={13} className="text-amber-500 dark:text-amber-400" />}
|
|
153
153
|
{liveCount > 0 && (
|
|
154
154
|
<span
|
|
155
|
-
|
|
155
|
+
data-cv-tooltip={labels.liveCountTooltip(liveCount)}
|
|
156
156
|
className="font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs"
|
|
157
157
|
>
|
|
158
158
|
{liveCount}
|
|
@@ -181,7 +181,7 @@ export function FlowNodePanel({
|
|
|
181
181
|
<div className="fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col">
|
|
182
182
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700">
|
|
183
183
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{labels.nodePanel.title}</h3>
|
|
184
|
-
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
184
|
+
<button data-cv-tooltip={labels.nodePanel.close} aria-label={labels.nodePanel.close} onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
185
185
|
<X size={18} />
|
|
186
186
|
</button>
|
|
187
187
|
</div>
|
|
@@ -358,7 +358,7 @@ export function FlowNodePanel({
|
|
|
358
358
|
<div>
|
|
359
359
|
<div className="flex items-center justify-between mb-1.5">
|
|
360
360
|
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">{labels.nodePanel.options}</label>
|
|
361
|
-
<button onClick={addOption} className="text-xs text-blue-600 hover:underline flex items-center gap-1">
|
|
361
|
+
<button data-cv-tooltip={labels.nodePanel.addOption} aria-label={labels.nodePanel.addOption} onClick={addOption} className="text-xs text-blue-600 hover:underline flex items-center gap-1">
|
|
362
362
|
<Plus size={12} /> {labels.nodePanel.addOption}
|
|
363
363
|
</button>
|
|
364
364
|
</div>
|
|
@@ -377,7 +377,7 @@ export function FlowNodePanel({
|
|
|
377
377
|
placeholder={labels.nodePanel.optionLabel}
|
|
378
378
|
className={`flex-1 ${INPUT_CLASSNAME}`}
|
|
379
379
|
/>
|
|
380
|
-
<button onClick={() => removeOption(i)} className="text-gray-400 hover:text-red-600">
|
|
380
|
+
<button data-cv-tooltip={labels.nodePanel.removeOption} aria-label={labels.nodePanel.removeOption} onClick={() => removeOption(i)} className="text-gray-400 hover:text-red-600">
|
|
381
381
|
<Trash2 size={14} />
|
|
382
382
|
</button>
|
|
383
383
|
</div>
|
|
@@ -441,6 +441,7 @@ export function FlowNodePanel({
|
|
|
441
441
|
|
|
442
442
|
<div className="p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2">
|
|
443
443
|
<button
|
|
444
|
+
data-cv-tooltip={labels.nodePanel.save} aria-label={labels.nodePanel.save}
|
|
444
445
|
onClick={() => onChange(draft)}
|
|
445
446
|
className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
446
447
|
>
|
|
@@ -451,13 +452,13 @@ export function FlowNodePanel({
|
|
|
451
452
|
onClick={() => {
|
|
452
453
|
if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id)
|
|
453
454
|
}}
|
|
454
|
-
|
|
455
|
+
data-cv-tooltip={labels.nodePanel.delete} aria-label={labels.nodePanel.delete}
|
|
455
456
|
className="px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg"
|
|
456
457
|
>
|
|
457
458
|
<Trash2 size={14} />
|
|
458
459
|
</button>
|
|
459
460
|
)}
|
|
460
|
-
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline">
|
|
461
|
+
<button data-cv-tooltip={labels.nodePanel.cancel} aria-label={labels.nodePanel.cancel} onClick={onClose} className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline">
|
|
461
462
|
{labels.nodePanel.cancel}
|
|
462
463
|
</button>
|
|
463
464
|
</div>
|
|
@@ -58,6 +58,7 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
58
58
|
return (
|
|
59
59
|
<div ref={containerRef} className="relative">
|
|
60
60
|
<button
|
|
61
|
+
data-cv-tooltip={labels.palette.title} aria-label={labels.palette.title}
|
|
61
62
|
onClick={() => setOpen((v) => !v)}
|
|
62
63
|
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
|
|
63
64
|
>
|
|
@@ -68,6 +69,7 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
68
69
|
<div className="absolute left-0 top-full mt-1.5 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg z-50 py-1">
|
|
69
70
|
<div className="relative">
|
|
70
71
|
<button
|
|
72
|
+
data-cv-tooltip={labels.palette.question} aria-label={labels.palette.question}
|
|
71
73
|
onMouseEnter={() => setSubmenu('question')}
|
|
72
74
|
onClick={() => setSubmenu(submenu === 'question' ? null : 'question')}
|
|
73
75
|
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
@@ -81,6 +83,7 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
81
83
|
<div className="absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1">
|
|
82
84
|
{QUESTION_TYPES.map((qt) => (
|
|
83
85
|
<button
|
|
86
|
+
data-cv-tooltip={labels.questionTypeLabels[qt]} aria-label={labels.questionTypeLabels[qt]}
|
|
84
87
|
key={qt}
|
|
85
88
|
onClick={() => select({ kind: 'question', questionType: qt })}
|
|
86
89
|
className="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
@@ -93,6 +96,7 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
93
96
|
</div>
|
|
94
97
|
|
|
95
98
|
<button
|
|
99
|
+
data-cv-tooltip={labels.palette.decision} aria-label={labels.palette.decision}
|
|
96
100
|
onMouseEnter={() => setSubmenu(null)}
|
|
97
101
|
onClick={() => select({ kind: 'decision' })}
|
|
98
102
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
@@ -104,13 +108,14 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
104
108
|
onMouseEnter={() => setSubmenu(null)}
|
|
105
109
|
onClick={() => select({ kind: 'condition' })}
|
|
106
110
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
107
|
-
|
|
111
|
+
data-cv-tooltip={labels.palette.conditionHint} aria-label={labels.palette.conditionHint}
|
|
108
112
|
>
|
|
109
113
|
<Diamond size={15} className="text-cyan-500" /> {labels.palette.condition}
|
|
110
114
|
</button>
|
|
111
115
|
|
|
112
116
|
<div className="relative">
|
|
113
117
|
<button
|
|
118
|
+
data-cv-tooltip={labels.palette.action} aria-label={labels.palette.action}
|
|
114
119
|
onMouseEnter={() => setSubmenu('action')}
|
|
115
120
|
onClick={() => setSubmenu(submenu === 'action' ? null : 'action')}
|
|
116
121
|
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
@@ -124,6 +129,7 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
124
129
|
<div className="absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1">
|
|
125
130
|
{resolvedActionOptions.map((option) => (
|
|
126
131
|
<button
|
|
132
|
+
data-cv-tooltip={option.label} aria-label={option.label}
|
|
127
133
|
key={option.actionKind}
|
|
128
134
|
onClick={() => select({ kind: 'action', actionKind: option.actionKind })}
|
|
129
135
|
className="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
@@ -16,7 +16,7 @@ export function FlowPortalNode({ data }: NodeProps) {
|
|
|
16
16
|
return (
|
|
17
17
|
<button
|
|
18
18
|
type="button"
|
|
19
|
-
|
|
19
|
+
data-cv-tooltip={labels.crossFlowPortal.tooltip} aria-label={labels.crossFlowPortal.tooltip}
|
|
20
20
|
onClick={onNavigate}
|
|
21
21
|
className="relative flex items-center gap-1.5 rounded-full border-2 border-dashed border-cyan-400 dark:border-cyan-600 bg-cyan-50 dark:bg-cyan-950/40 px-3 py-1.5 text-xs font-medium text-cyan-700 dark:text-cyan-300 hover:bg-cyan-100 dark:hover:bg-cyan-950/70 transition-colors cursor-pointer"
|
|
22
22
|
>
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type FlowNodeData,
|
|
41
41
|
type GraphIssue,
|
|
42
42
|
} from './flowGraph'
|
|
43
|
+
import { TooltipLayer } from '../Tooltip'
|
|
43
44
|
|
|
44
45
|
const RF_NODE_TYPES = {
|
|
45
46
|
...flowNodeTypes,
|
|
@@ -966,6 +967,7 @@ export function FlowsWorkspace({
|
|
|
966
967
|
|
|
967
968
|
return (
|
|
968
969
|
<div className={`space-y-4 h-full flex flex-col ${className ?? ''}`}>
|
|
970
|
+
<TooltipLayer />
|
|
969
971
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
|
970
972
|
<div>
|
|
971
973
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{labels.workspace.title}</h2>
|
|
@@ -977,6 +979,7 @@ export function FlowsWorkspace({
|
|
|
977
979
|
<span className="text-sm text-red-600">{saveErrorMessage ?? labels.workspace.saveError}</span>
|
|
978
980
|
)}
|
|
979
981
|
<button
|
|
982
|
+
data-cv-tooltip={viewMode === 'map' ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap} aria-label={viewMode === 'map' ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap}
|
|
980
983
|
type="button"
|
|
981
984
|
onClick={() => setViewMode((mode) => (mode === 'map' ? 'detail' : 'map'))}
|
|
982
985
|
className={OUTLINE_BUTTON}
|
|
@@ -990,7 +993,7 @@ export function FlowsWorkspace({
|
|
|
990
993
|
type="button"
|
|
991
994
|
onClick={handleOrganize}
|
|
992
995
|
className={OUTLINE_BUTTON}
|
|
993
|
-
|
|
996
|
+
data-cv-tooltip={labels.workspace.organizeTooltip} aria-label={labels.workspace.organizeTooltip}
|
|
994
997
|
disabled={!primaryGraph}
|
|
995
998
|
>
|
|
996
999
|
<LayoutGrid size={14} aria-hidden="true" /> {labels.workspace.organize}
|
|
@@ -999,12 +1002,13 @@ export function FlowsWorkspace({
|
|
|
999
1002
|
type="button"
|
|
1000
1003
|
onClick={handleDiscardChanges}
|
|
1001
1004
|
className={OUTLINE_BUTTON}
|
|
1002
|
-
|
|
1005
|
+
data-cv-tooltip={labels.workspace.discardTooltip} aria-label={labels.workspace.discardTooltip}
|
|
1003
1006
|
disabled={!isDirty || saveState === 'saving'}
|
|
1004
1007
|
>
|
|
1005
1008
|
<Undo2 size={14} aria-hidden="true" /> {labels.workspace.discardChanges}
|
|
1006
1009
|
</button>
|
|
1007
1010
|
<button
|
|
1011
|
+
data-cv-tooltip={labels.workspace.saveGraph} aria-label={labels.workspace.saveGraph}
|
|
1008
1012
|
type="button"
|
|
1009
1013
|
onClick={() => void handlePublish()}
|
|
1010
1014
|
className={PRIMARY_BUTTON}
|
|
@@ -1024,6 +1028,7 @@ export function FlowsWorkspace({
|
|
|
1024
1028
|
{graphs &&
|
|
1025
1029
|
Object.values(graphs).map((graph) => (
|
|
1026
1030
|
<button
|
|
1031
|
+
data-cv-tooltip={graph.label} aria-label={graph.label}
|
|
1027
1032
|
key={graph.key}
|
|
1028
1033
|
type="button"
|
|
1029
1034
|
onClick={() => focusFlow(graph.key)}
|
|
@@ -1038,6 +1043,7 @@ export function FlowsWorkspace({
|
|
|
1038
1043
|
))}
|
|
1039
1044
|
{canCreateFlow && (
|
|
1040
1045
|
<button
|
|
1046
|
+
data-cv-tooltip={labels.flowManager.newFlow} aria-label={labels.flowManager.newFlow}
|
|
1041
1047
|
type="button"
|
|
1042
1048
|
onClick={() => {
|
|
1043
1049
|
setFlowMutationState({ pending: false })
|
|
@@ -1059,6 +1065,7 @@ export function FlowsWorkspace({
|
|
|
1059
1065
|
)}
|
|
1060
1066
|
{primaryGraph && canDeleteFlow && (
|
|
1061
1067
|
<button
|
|
1068
|
+
data-cv-tooltip={labels.flowManager.deleteFlow} aria-label={labels.flowManager.deleteFlow}
|
|
1062
1069
|
type="button"
|
|
1063
1070
|
onClick={() => {
|
|
1064
1071
|
setFlowMutationState({ pending: false })
|
|
@@ -1186,10 +1193,11 @@ export function FlowsWorkspace({
|
|
|
1186
1193
|
{flowMutationState.error && <p className="text-xs text-red-600">{flowMutationState.error}</p>}
|
|
1187
1194
|
</div>
|
|
1188
1195
|
<div className="flex justify-end gap-2 mt-4">
|
|
1189
|
-
<button type="button" className={OUTLINE_BUTTON} onClick={() => setShowCreateDialog(false)}>
|
|
1196
|
+
<button data-cv-tooltip={labels.nodePanel.cancel} aria-label={labels.nodePanel.cancel} type="button" className={OUTLINE_BUTTON} onClick={() => setShowCreateDialog(false)}>
|
|
1190
1197
|
{labels.nodePanel.cancel}
|
|
1191
1198
|
</button>
|
|
1192
1199
|
<button
|
|
1200
|
+
data-cv-tooltip={labels.flowManager.create} aria-label={labels.flowManager.create}
|
|
1193
1201
|
type="button"
|
|
1194
1202
|
className={PRIMARY_BUTTON}
|
|
1195
1203
|
onClick={() => void handleCreateFlow()}
|
|
@@ -1208,10 +1216,11 @@ export function FlowsWorkspace({
|
|
|
1208
1216
|
</p>
|
|
1209
1217
|
{flowMutationState.error && <p className="text-xs text-red-600 mt-2">{flowMutationState.error}</p>}
|
|
1210
1218
|
<div className="flex justify-end gap-2 mt-4">
|
|
1211
|
-
<button type="button" className={OUTLINE_BUTTON} onClick={() => setShowDeleteDialog(false)}>
|
|
1219
|
+
<button data-cv-tooltip={labels.nodePanel.cancel} aria-label={labels.nodePanel.cancel} type="button" className={OUTLINE_BUTTON} onClick={() => setShowDeleteDialog(false)}>
|
|
1212
1220
|
{labels.nodePanel.cancel}
|
|
1213
1221
|
</button>
|
|
1214
1222
|
<button
|
|
1223
|
+
data-cv-tooltip={labels.flowManager.deleteFlow} aria-label={labels.flowManager.deleteFlow}
|
|
1215
1224
|
type="button"
|
|
1216
1225
|
className="inline-flex items-center gap-1.5 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 disabled:opacity-40"
|
|
1217
1226
|
onClick={() => void handleDeleteFlow()}
|