@adatechnology/conversations-ui 0.1.0-rc.27 → 0.1.0-rc.29
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-UZJBYD5O.js → chunk-GY472G6E.js} +56 -5
- package/dist/flows/index.d.ts +73 -2
- package/dist/flows/index.js +115 -77
- package/dist/index.d.ts +6 -2
- package/dist/index.js +68 -18
- package/dist/preview/index.d.ts +2 -2
- package/dist/preview/index.js +1 -1
- package/dist/{types-BfINicc-.d.ts → types-De5aN-E_.d.ts} +11 -0
- package/package.json +2 -2
- package/src/DocumentsLibrary.tsx +58 -3
- package/src/buildOutput.test.ts +79 -0
- package/src/documents/DocumentsWorkspace.tsx +52 -2
- package/src/documents/labels.ts +4 -0
- package/src/flows/FlowsWorkspace.tsx +56 -98
- 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/workspaceContract.test.ts +95 -0
- package/src/providers/types.ts +11 -0
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
useConversationDocuments,
|
|
58
58
|
useConversationLocales,
|
|
59
59
|
useConversations
|
|
60
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-GY472G6E.js";
|
|
61
61
|
import {
|
|
62
62
|
ZERO_WIDTH_SPACE,
|
|
63
63
|
htmlToWA,
|
|
@@ -1869,8 +1869,8 @@ function isWindowBlocking(window2) {
|
|
|
1869
1869
|
}
|
|
1870
1870
|
|
|
1871
1871
|
// src/documents/DocumentsWorkspace.tsx
|
|
1872
|
-
import { useEffect as useEffect7, useMemo as useMemo2, useState as useState11 } from "react";
|
|
1873
|
-
import { Download as Download2, Eye, MessageSquare, Trash2, X as X3 } from "lucide-react";
|
|
1872
|
+
import { useEffect as useEffect7, useMemo as useMemo2, useRef as useRef5, useState as useState11 } from "react";
|
|
1873
|
+
import { Download as Download2, Eye, MessageSquare, Trash2, Upload, X as X3 } from "lucide-react";
|
|
1874
1874
|
|
|
1875
1875
|
// src/listing/index.tsx
|
|
1876
1876
|
import { useState as useState9 } from "react";
|
|
@@ -2092,6 +2092,8 @@ var DEFAULT_DOCUMENTS_WORKSPACE_LABELS = {
|
|
|
2092
2092
|
bulkDownloadZip: "Baixar em zip",
|
|
2093
2093
|
bulkRemove: (count) => `Excluir ${count}`,
|
|
2094
2094
|
bulkRemoveConfirm: (count) => `Excluir ${count} arquivo${count === 1 ? "" : "s"}?`,
|
|
2095
|
+
upload: "Enviar documento",
|
|
2096
|
+
uploadError: "N\xE3o foi poss\xEDvel enviar o arquivo.",
|
|
2095
2097
|
columnFilename: "Arquivo",
|
|
2096
2098
|
columnContact: "Contato",
|
|
2097
2099
|
columnType: "Tipo",
|
|
@@ -2147,10 +2149,14 @@ function DocumentsWorkspace({
|
|
|
2147
2149
|
const [selectedIds, setSelectedIds] = useState11(/* @__PURE__ */ new Set());
|
|
2148
2150
|
const [reloadToken, setReloadToken] = useState11(0);
|
|
2149
2151
|
const [busy, setBusy] = useState11(false);
|
|
2152
|
+
const [uploading, setUploading] = useState11(false);
|
|
2153
|
+
const [uploadFailed, setUploadFailed] = useState11(false);
|
|
2154
|
+
const fileInputRef = useRef5(null);
|
|
2150
2155
|
const debouncedSearch = useDebouncedValue(search);
|
|
2151
2156
|
const fetchAll = context?.api.getAllDocuments;
|
|
2152
2157
|
const removeDocument = context?.api.deleteDocument;
|
|
2153
2158
|
const downloadArchive = context?.api.downloadDocumentsArchiveByIds;
|
|
2159
|
+
const uploadDocument = context?.api.uploadDocument;
|
|
2154
2160
|
const extraKey = JSON.stringify(extra);
|
|
2155
2161
|
const sourceOptions = useMemo2(
|
|
2156
2162
|
() => sources ?? Object.entries(labels.sourceLabels).map(([value, label]) => ({ value, label })),
|
|
@@ -2234,6 +2240,19 @@ function DocumentsWorkspace({
|
|
|
2234
2240
|
setBusy(false);
|
|
2235
2241
|
}
|
|
2236
2242
|
}
|
|
2243
|
+
async function handleUpload(file) {
|
|
2244
|
+
if (!uploadDocument) return;
|
|
2245
|
+
setUploading(true);
|
|
2246
|
+
setUploadFailed(false);
|
|
2247
|
+
try {
|
|
2248
|
+
await uploadDocument(file, Object.keys(extra).length > 0 ? extra : void 0);
|
|
2249
|
+
setReloadToken((token) => token + 1);
|
|
2250
|
+
} catch {
|
|
2251
|
+
setUploadFailed(true);
|
|
2252
|
+
} finally {
|
|
2253
|
+
setUploading(false);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2237
2256
|
async function handleDownloadArchive() {
|
|
2238
2257
|
if (!downloadArchive) return;
|
|
2239
2258
|
setBusy(true);
|
|
@@ -2307,8 +2326,39 @@ function DocumentsWorkspace({
|
|
|
2307
2326
|
hasFilters ? /* @__PURE__ */ jsxs14("button", { "data-cv-tooltip": labels.clearFilters, "aria-label": labels.clearFilters, type: "button", onClick: clearFilters, className: "cv-header-action inline-flex items-center gap-1", children: [
|
|
2308
2327
|
/* @__PURE__ */ jsx16(X3, { size: 12, "aria-hidden": "true" }),
|
|
2309
2328
|
labels.clearFilters
|
|
2329
|
+
] }) : null,
|
|
2330
|
+
uploadDocument ? /* @__PURE__ */ jsxs14(Fragment5, { children: [
|
|
2331
|
+
/* @__PURE__ */ jsx16(
|
|
2332
|
+
"input",
|
|
2333
|
+
{
|
|
2334
|
+
ref: fileInputRef,
|
|
2335
|
+
type: "file",
|
|
2336
|
+
hidden: true,
|
|
2337
|
+
onChange: (event) => {
|
|
2338
|
+
const file = event.target.files?.[0];
|
|
2339
|
+
event.target.value = "";
|
|
2340
|
+
if (file) void handleUpload(file);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
),
|
|
2344
|
+
/* @__PURE__ */ jsxs14(
|
|
2345
|
+
"button",
|
|
2346
|
+
{
|
|
2347
|
+
"data-cv-tooltip": labels.upload,
|
|
2348
|
+
"aria-label": labels.upload,
|
|
2349
|
+
type: "button",
|
|
2350
|
+
onClick: () => fileInputRef.current?.click(),
|
|
2351
|
+
disabled: uploading,
|
|
2352
|
+
className: "cv-header-action ml-auto inline-flex items-center gap-1 disabled:opacity-40",
|
|
2353
|
+
children: [
|
|
2354
|
+
/* @__PURE__ */ jsx16(Upload, { size: 12, "aria-hidden": "true" }),
|
|
2355
|
+
labels.upload
|
|
2356
|
+
]
|
|
2357
|
+
}
|
|
2358
|
+
)
|
|
2310
2359
|
] }) : null
|
|
2311
2360
|
] }),
|
|
2361
|
+
uploadFailed ? /* @__PURE__ */ jsx16("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.uploadError }) : null,
|
|
2312
2362
|
canSelect ? /* @__PURE__ */ jsxs14(
|
|
2313
2363
|
BulkActionBar,
|
|
2314
2364
|
{
|
|
@@ -2516,7 +2566,7 @@ function downloadTextFile(filename, content) {
|
|
|
2516
2566
|
}
|
|
2517
2567
|
|
|
2518
2568
|
// src/useWaitingNotifications.ts
|
|
2519
|
-
import { useState as useState12, useEffect as useEffect8, useRef as
|
|
2569
|
+
import { useState as useState12, useEffect as useEffect8, useRef as useRef6, useCallback as useCallback5 } from "react";
|
|
2520
2570
|
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
2521
2571
|
var DEFAULT_LIMIT = 50;
|
|
2522
2572
|
var DEFAULT_LABELS = {
|
|
@@ -2526,9 +2576,9 @@ var DEFAULT_LABELS = {
|
|
|
2526
2576
|
function useWaitingNotifications(params) {
|
|
2527
2577
|
const conversationsContext = useConversations();
|
|
2528
2578
|
const [conversations, setConversations] = useState12([]);
|
|
2529
|
-
const previousUnreadMap =
|
|
2530
|
-
const notifiedIds =
|
|
2531
|
-
const isPollingRef =
|
|
2579
|
+
const previousUnreadMap = useRef6(/* @__PURE__ */ new Map());
|
|
2580
|
+
const notifiedIds = useRef6(/* @__PURE__ */ new Set());
|
|
2581
|
+
const isPollingRef = useRef6(false);
|
|
2532
2582
|
const isEnabled = params?.enabled ?? true;
|
|
2533
2583
|
const intervalMs = params?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
2534
2584
|
const icon = params?.icon;
|
|
@@ -3708,16 +3758,16 @@ function useConversationMessages(conversationId, params) {
|
|
|
3708
3758
|
}
|
|
3709
3759
|
|
|
3710
3760
|
// src/hooks/useScrollToLatestMessage.ts
|
|
3711
|
-
import { useCallback as useCallback8, useLayoutEffect, useRef as
|
|
3761
|
+
import { useCallback as useCallback8, useLayoutEffect, useRef as useRef7, useState as useState15 } from "react";
|
|
3712
3762
|
var NEAR_BOTTOM_THRESHOLD_PX = 120;
|
|
3713
3763
|
var SCROLL_BEHAVIOR = "auto";
|
|
3714
3764
|
function useScrollToLatestMessage({
|
|
3715
3765
|
conversationId,
|
|
3716
3766
|
messageCount
|
|
3717
3767
|
}) {
|
|
3718
|
-
const containerRef =
|
|
3768
|
+
const containerRef = useRef7(null);
|
|
3719
3769
|
const [isAwayFromBottom, setIsAwayFromBottom] = useState15(false);
|
|
3720
|
-
const isAwayFromBottomRef =
|
|
3770
|
+
const isAwayFromBottomRef = useRef7(false);
|
|
3721
3771
|
const scrollToBottom = useCallback8((behavior = SCROLL_BEHAVIOR) => {
|
|
3722
3772
|
const container = containerRef.current;
|
|
3723
3773
|
if (!container) return;
|
|
@@ -3730,7 +3780,7 @@ function useScrollToLatestMessage({
|
|
|
3730
3780
|
isAwayFromBottomRef.current = away;
|
|
3731
3781
|
setIsAwayFromBottom((current) => current === away ? current : away);
|
|
3732
3782
|
}, []);
|
|
3733
|
-
const jumpedForConversationRef =
|
|
3783
|
+
const jumpedForConversationRef = useRef7(void 0);
|
|
3734
3784
|
useLayoutEffect(() => {
|
|
3735
3785
|
const isNewConversation = jumpedForConversationRef.current !== conversationId;
|
|
3736
3786
|
if (isNewConversation) {
|
|
@@ -3777,10 +3827,10 @@ function useConversationContext(conversationId) {
|
|
|
3777
3827
|
}
|
|
3778
3828
|
|
|
3779
3829
|
// src/hooks/useConversationRealtime.ts
|
|
3780
|
-
import { useEffect as useEffect10, useRef as
|
|
3830
|
+
import { useEffect as useEffect10, useRef as useRef8 } from "react";
|
|
3781
3831
|
function useConversationRealtime(conversationId, onEvent) {
|
|
3782
3832
|
const sse = useConversations()?.sse;
|
|
3783
|
-
const onEventRef =
|
|
3833
|
+
const onEventRef = useRef8(onEvent);
|
|
3784
3834
|
onEventRef.current = onEvent;
|
|
3785
3835
|
useEffect10(() => {
|
|
3786
3836
|
if (!sse || !conversationId) return;
|
|
@@ -3795,7 +3845,7 @@ function useConversationRealtime(conversationId, onEvent) {
|
|
|
3795
3845
|
}
|
|
3796
3846
|
function useGlobalRealtime(onEvent) {
|
|
3797
3847
|
const sse = useConversations()?.sse;
|
|
3798
|
-
const onEventRef =
|
|
3848
|
+
const onEventRef = useRef8(onEvent);
|
|
3799
3849
|
onEventRef.current = onEvent;
|
|
3800
3850
|
useEffect10(() => {
|
|
3801
3851
|
if (!sse) return;
|
|
@@ -3937,7 +3987,7 @@ function BulkTemplateModal({
|
|
|
3937
3987
|
}
|
|
3938
3988
|
|
|
3939
3989
|
// src/workspace/ConversationPane.tsx
|
|
3940
|
-
import { useEffect as useEffect12, useRef as
|
|
3990
|
+
import { useEffect as useEffect12, useRef as useRef9, useState as useState17 } from "react";
|
|
3941
3991
|
import { jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
3942
3992
|
function ConversationPane({
|
|
3943
3993
|
conversation,
|
|
@@ -3977,7 +4027,7 @@ function ConversationPane({
|
|
|
3977
4027
|
const [draft, setDraft] = useState17(initialComposerText ?? "");
|
|
3978
4028
|
const [queuedFiles, setQueuedFiles] = useState17([]);
|
|
3979
4029
|
const [isSendingDraft, setIsSendingDraft] = useState17(false);
|
|
3980
|
-
const sendInFlightRef =
|
|
4030
|
+
const sendInFlightRef = useRef9(false);
|
|
3981
4031
|
useEffect12(() => {
|
|
3982
4032
|
setSelectedMessageIds(/* @__PURE__ */ new Set());
|
|
3983
4033
|
setDraft(initialComposerText ?? "");
|
|
@@ -4383,7 +4433,7 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
|
|
|
4383
4433
|
};
|
|
4384
4434
|
|
|
4385
4435
|
// src/workspace/useConversationsInbox.ts
|
|
4386
|
-
import { useCallback as useCallback9, useEffect as useEffect13, useMemo as useMemo5, useRef as
|
|
4436
|
+
import { useCallback as useCallback9, useEffect as useEffect13, useMemo as useMemo5, useRef as useRef10, useState as useState18 } from "react";
|
|
4387
4437
|
var CONVERSATIONS_PER_PAGE = 50;
|
|
4388
4438
|
function defaultDescribeFailure(error) {
|
|
4389
4439
|
const status = error.status;
|
|
@@ -4441,7 +4491,7 @@ function useConversationsInbox(params = {}) {
|
|
|
4441
4491
|
setSelectedId(void 0);
|
|
4442
4492
|
}
|
|
4443
4493
|
}, [conversations, selectedId]);
|
|
4444
|
-
const lastMarkReadAttempt =
|
|
4494
|
+
const lastMarkReadAttempt = useRef10(void 0);
|
|
4445
4495
|
useEffect13(() => {
|
|
4446
4496
|
if (!markReadOnOpen || !selectedId) return;
|
|
4447
4497
|
const opened = conversations.find((conversation) => conversation.id === selectedId);
|
package/dist/preview/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { G as MessagePayload, o as ConversationSummary, m as ConversationEventSource, q as ConversationsApi, L as ListConversationsParams, n as ConversationPage, S as SSEProvider, k as ConversationDocument, N as ResolveMediaUrl } from '../types-
|
|
2
|
-
export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS } from '../types-
|
|
1
|
+
import { G as MessagePayload, o as ConversationSummary, m as ConversationEventSource, q as ConversationsApi, L as ListConversationsParams, n as ConversationPage, S as SSEProvider, k as ConversationDocument, N as ResolveMediaUrl } from '../types-De5aN-E_.js';
|
|
2
|
+
export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS } from '../types-De5aN-E_.js';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
5
|
import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
|
package/dist/preview/index.js
CHANGED
|
@@ -396,6 +396,17 @@ interface ConversationsApi {
|
|
|
396
396
|
* `downloadDocumentsArchive`, que é por conversa. Ausente, a seleção em lote não oferece o botão.
|
|
397
397
|
*/
|
|
398
398
|
downloadDocumentsArchiveByIds?(uploadIds: readonly string[]): Promise<Blob>;
|
|
399
|
+
/**
|
|
400
|
+
* Envia um arquivo avulso direto pra biblioteca, fora do fluxo de uma conversa. **Opcional por
|
|
401
|
+
* capacidade:** cada host tem seu próprio contrato de upload (base64, multipart, presigned URL) —
|
|
402
|
+
* o pacote não escolhe um formato de payload, só entrega o `File` do input e deixa o host montar
|
|
403
|
+
* a chamada do jeito que seu backend espera. Ausente, a tela de biblioteca não desenha o botão de
|
|
404
|
+
* enviar, em vez de oferecer uma ação que sempre falha.
|
|
405
|
+
*
|
|
406
|
+
* `extra` é o mesmo vocabulário livre do produto que já viaja em `renderFilters` — cliente,
|
|
407
|
+
* unidade, campanha — pra associar o arquivo enviado ao contexto que a tela estava filtrando.
|
|
408
|
+
*/
|
|
409
|
+
uploadDocument?(file: File, extra?: Readonly<Record<string, string | number>>): Promise<ConversationDocument>;
|
|
399
410
|
getMediaProxyUrl(mediaId: string): Promise<{
|
|
400
411
|
mimeType: string;
|
|
401
412
|
data: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.29",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@types/react-dom": "^18 || ^19"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
|
-
"build": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/preview/index.ts src/styles.css --dts --format esm --external react --external react-dom --external @xyflow/react",
|
|
54
|
+
"build": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/preview/index.ts src/styles.css --dts --clean --format esm --external react --external react-dom --external @xyflow/react",
|
|
55
55
|
"build:watch": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/styles.css --watch --dts --format esm --external react --external react-dom --external @xyflow/react",
|
|
56
56
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
57
57
|
"test": "bun test"
|
package/src/DocumentsLibrary.tsx
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* clicável. Sem essa referência, uma lista global de anexos não responde nenhuma pergunta.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { useEffect, useState } from 'react'
|
|
11
|
-
import { ArrowUpDown, Bot, Download, Eye, MessageSquare, Users } from 'lucide-react'
|
|
10
|
+
import { useEffect, useRef, useState } from 'react'
|
|
11
|
+
import { ArrowUpDown, Bot, Download, Eye, MessageSquare, Upload, Users } from 'lucide-react'
|
|
12
12
|
import { useConversations } from './providers/ConversationsProvider'
|
|
13
13
|
import { DOCUMENT_SOURCE_FILTER, type DocumentSourceFilter } from './ConversationDocumentsPanel'
|
|
14
14
|
import { FileIcon } from './FileIcon'
|
|
@@ -34,6 +34,8 @@ export interface DocumentsLibraryLabels {
|
|
|
34
34
|
sortMostRecent: string
|
|
35
35
|
sortOldest: string
|
|
36
36
|
clearFilters: string
|
|
37
|
+
upload: string
|
|
38
|
+
uploadError: string
|
|
37
39
|
total: (count: number) => string
|
|
38
40
|
page: (current: number, last: number) => string
|
|
39
41
|
}
|
|
@@ -54,6 +56,8 @@ export const DEFAULT_DOCUMENTS_LIBRARY_LABELS: DocumentsLibraryLabels = {
|
|
|
54
56
|
sortMostRecent: 'Mais recentes',
|
|
55
57
|
sortOldest: 'Mais antigos',
|
|
56
58
|
clearFilters: 'Limpar filtros',
|
|
59
|
+
upload: 'Enviar documento',
|
|
60
|
+
uploadError: 'Não foi possível enviar o arquivo.',
|
|
57
61
|
total: (count: number) => `${count} arquivo${count === 1 ? '' : 's'}`,
|
|
58
62
|
page: (current: number, last: number) => `${current} / ${last}`,
|
|
59
63
|
}
|
|
@@ -106,9 +110,15 @@ export function DocumentsLibrary({
|
|
|
106
110
|
const [loading, setLoading] = useState(false)
|
|
107
111
|
const [failed, setFailed] = useState(false)
|
|
108
112
|
|
|
113
|
+
const [uploading, setUploading] = useState(false)
|
|
114
|
+
const [uploadFailed, setUploadFailed] = useState(false)
|
|
115
|
+
const [reloadToken, setReloadToken] = useState(0)
|
|
116
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
117
|
+
|
|
109
118
|
const hasFilters = search !== '' || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== 'desc'
|
|
110
119
|
const lastPage = Math.max(1, Math.ceil(total / perPage))
|
|
111
120
|
const fetchAll = context?.api.getAllDocuments
|
|
121
|
+
const uploadDocument = context?.api.uploadDocument
|
|
112
122
|
|
|
113
123
|
useEffect(() => {
|
|
114
124
|
if (!fetchAll) return
|
|
@@ -140,7 +150,7 @@ export function DocumentsLibrary({
|
|
|
140
150
|
return () => {
|
|
141
151
|
active = false
|
|
142
152
|
}
|
|
143
|
-
}, [fetchAll, search, sourceFilter, sortDirection, page, perPage])
|
|
153
|
+
}, [fetchAll, search, sourceFilter, sortDirection, page, perPage, reloadToken])
|
|
144
154
|
|
|
145
155
|
function applyFilter(change: () => void): void {
|
|
146
156
|
change()
|
|
@@ -152,6 +162,20 @@ export function DocumentsLibrary({
|
|
|
152
162
|
if (url) window.open(url, '_blank', 'noopener,noreferrer')
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
async function handleUpload(file: File): Promise<void> {
|
|
166
|
+
if (!uploadDocument) return
|
|
167
|
+
setUploading(true)
|
|
168
|
+
setUploadFailed(false)
|
|
169
|
+
try {
|
|
170
|
+
await uploadDocument(file)
|
|
171
|
+
setReloadToken((token) => token + 1)
|
|
172
|
+
} catch {
|
|
173
|
+
setUploadFailed(true)
|
|
174
|
+
} finally {
|
|
175
|
+
setUploading(false)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
155
179
|
// Host sem `getAllDocuments` não tem o que mostrar aqui — some, em vez de renderizar vazio para
|
|
156
180
|
// sempre e fazer parecer que a empresa não tem arquivo nenhum.
|
|
157
181
|
if (!fetchAll) return null
|
|
@@ -207,6 +231,32 @@ export function DocumentsLibrary({
|
|
|
207
231
|
{labels.clearFilters}
|
|
208
232
|
</button>
|
|
209
233
|
) : null}
|
|
234
|
+
|
|
235
|
+
{uploadDocument ? (
|
|
236
|
+
<>
|
|
237
|
+
<input
|
|
238
|
+
ref={fileInputRef}
|
|
239
|
+
type="file"
|
|
240
|
+
hidden
|
|
241
|
+
onChange={(event) => {
|
|
242
|
+
const file = event.target.files?.[0]
|
|
243
|
+
event.target.value = ''
|
|
244
|
+
if (file) void handleUpload(file)
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
<button
|
|
248
|
+
data-cv-tooltip={labels.upload}
|
|
249
|
+
aria-label={labels.upload}
|
|
250
|
+
type="button"
|
|
251
|
+
onClick={() => fileInputRef.current?.click()}
|
|
252
|
+
disabled={uploading}
|
|
253
|
+
className="cv-header-action ml-auto inline-flex items-center gap-1 disabled:opacity-40"
|
|
254
|
+
>
|
|
255
|
+
<Upload size={14} aria-hidden="true" />
|
|
256
|
+
{labels.upload}
|
|
257
|
+
</button>
|
|
258
|
+
</>
|
|
259
|
+
) : null}
|
|
210
260
|
</div>
|
|
211
261
|
|
|
212
262
|
{loading ? <p className={cn('text-sm text-gray-500', classNames?.status)}>{labels.loading}</p> : null}
|
|
@@ -215,6 +265,11 @@ export function DocumentsLibrary({
|
|
|
215
265
|
{labels.failure}
|
|
216
266
|
</p>
|
|
217
267
|
) : null}
|
|
268
|
+
{uploadFailed ? (
|
|
269
|
+
<p role="alert" className={cn('text-sm text-red-600 dark:text-red-400', classNames?.status)}>
|
|
270
|
+
{labels.uploadError}
|
|
271
|
+
</p>
|
|
272
|
+
) : null}
|
|
218
273
|
{!loading && !failed && documents.length === 0 ? (
|
|
219
274
|
<p className={cn('text-sm text-gray-500', classNames?.status)}>
|
|
220
275
|
{hasFilters ? labels.noResults : labels.empty}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Ada Technology. MIT License.
|
|
3
|
+
*
|
|
4
|
+
* O único teste que lê o `dist` em vez do fonte.
|
|
5
|
+
*
|
|
6
|
+
* Existe porque o `notification-ui@rc.1` saiu com 19 testes verdes e não renderizava. Dois defeitos,
|
|
7
|
+
* os dois invisíveis para quem importa o fonte:
|
|
8
|
+
*
|
|
9
|
+
* 1. `jsx: react-jsx` chegava ao tsup por um tsconfig com `extends`, e o esbuild NÃO segue `extends`
|
|
10
|
+
* para essa opção. O bundle saiu com `React.createElement` sem `React` no escopo, e o produto
|
|
11
|
+
* quebrou com `ReferenceError: React is not defined`.
|
|
12
|
+
* 2. `splitting: false` com dois entrypoints duplicou o módulo de contexto. O provider de um bundle
|
|
13
|
+
* não era o mesmo objeto do consumidor no outro, e o hook acusava "usado fora do provider" estando
|
|
14
|
+
* dentro de um.
|
|
15
|
+
*
|
|
16
|
+
* Nenhum teste de fonte pega isso: eles importam `./index`, não `dist`. Rode depois do build.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it } from 'bun:test'
|
|
20
|
+
|
|
21
|
+
const DIST = `${import.meta.dir}/../dist`
|
|
22
|
+
|
|
23
|
+
async function distText(file: string): Promise<string> {
|
|
24
|
+
const handle = Bun.file(`${DIST}/${file}`)
|
|
25
|
+
expect(await handle.exists(), `${file} não existe — rode \`bun run build\` antes`).toBe(true)
|
|
26
|
+
return handle.text()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('transform de JSX', () => {
|
|
30
|
+
it('usa o runtime automático, e não React.createElement', async () => {
|
|
31
|
+
for (const file of ['index.js', 'flows/index.js', 'preview/index.js']) {
|
|
32
|
+
const content = await distText(file)
|
|
33
|
+
|
|
34
|
+
expect(content, `${file} com createElement`).not.toContain('React.createElement')
|
|
35
|
+
expect(content, `${file} sem jsx-runtime`).toContain('react/jsx-runtime')
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('divisão de código entre entrypoints', () => {
|
|
41
|
+
it('os entrypoints compartilham chunk em vez de duplicar módulo', async () => {
|
|
42
|
+
// Sem `splitting: true`, cada entrypoint carrega a própria cópia dos módulos comuns — e um
|
|
43
|
+
// contexto do React duplicado deixa de ser o mesmo objeto entre provider e consumidor.
|
|
44
|
+
const chunks = [...new Bun.Glob('chunk-*.js').scanSync({ cwd: DIST })]
|
|
45
|
+
|
|
46
|
+
expect(chunks.length, 'nenhum chunk compartilhado gerado').toBeGreaterThan(0)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('telas compostas chegam ao pacote publicado', () => {
|
|
51
|
+
it('o subpath raiz entrega MessagesWorkspace, no js e nos tipos', async () => {
|
|
52
|
+
expect(await distText('index.js')).toContain('MessagesWorkspace')
|
|
53
|
+
expect(await distText('index.d.ts')).toContain('MessagesWorkspace')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('o subpath /flows entrega FlowsWorkspace, no js e nos tipos', async () => {
|
|
57
|
+
// Export declarado no `index.ts` e ausente do `dist` é o modo de falhar mais barato de cometer e
|
|
58
|
+
// mais caro de descobrir: só aparece no produto, depois de publicar.
|
|
59
|
+
expect(await distText('flows/index.js')).toContain('FlowsWorkspace')
|
|
60
|
+
expect(await distText('flows/index.d.ts')).toContain('FlowsWorkspace')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('o CSS publicado traz as classes que as telas consomem', async () => {
|
|
64
|
+
const css = await distText('styles.css')
|
|
65
|
+
|
|
66
|
+
// A inbox depende destas classes do pacote (o editor de fluxos e a tela de mensagens estilizam por
|
|
67
|
+
// utilitários do host). Publicar sem elas deixa a tela montada e sem layout — e nada falha.
|
|
68
|
+
expect(css).toContain('.cv-workspace')
|
|
69
|
+
expect(css).toContain('.cv-workspace-modal')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('o bundle do host não paga por @xyflow/react sem pedir', () => {
|
|
74
|
+
it('o subpath raiz não puxa o xyflow', async () => {
|
|
75
|
+
// É o motivo de o editor viver em `/flows`: quem só usa a inbox não carrega a biblioteca de
|
|
76
|
+
// canvas, que é a maior dependência do pacote.
|
|
77
|
+
expect(await distText('index.js')).not.toContain('@xyflow/react')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* remontasse isso à mão voltaria a divergir dos outros — foi o que aconteceu antes desta tela existir.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
13
|
-
import { Download, Eye, MessageSquare, Trash2, X } from 'lucide-react'
|
|
12
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
13
|
+
import { Download, Eye, MessageSquare, Trash2, Upload, X } from 'lucide-react'
|
|
14
14
|
|
|
15
15
|
import { useConversations } from '../providers/ConversationsProvider'
|
|
16
16
|
import { FileIcon } from '../FileIcon'
|
|
@@ -106,11 +106,15 @@ export function DocumentsWorkspace({
|
|
|
106
106
|
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(new Set())
|
|
107
107
|
const [reloadToken, setReloadToken] = useState(0)
|
|
108
108
|
const [busy, setBusy] = useState(false)
|
|
109
|
+
const [uploading, setUploading] = useState(false)
|
|
110
|
+
const [uploadFailed, setUploadFailed] = useState(false)
|
|
111
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
109
112
|
|
|
110
113
|
const debouncedSearch = useDebouncedValue(search)
|
|
111
114
|
const fetchAll = context?.api.getAllDocuments
|
|
112
115
|
const removeDocument = context?.api.deleteDocument
|
|
113
116
|
const downloadArchive = context?.api.downloadDocumentsArchiveByIds
|
|
117
|
+
const uploadDocument = context?.api.uploadDocument
|
|
114
118
|
const extraKey = JSON.stringify(extra)
|
|
115
119
|
|
|
116
120
|
const sourceOptions = useMemo<readonly FilterOption[]>(
|
|
@@ -224,6 +228,20 @@ export function DocumentsWorkspace({
|
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
async function handleUpload(file: File): Promise<void> {
|
|
232
|
+
if (!uploadDocument) return
|
|
233
|
+
setUploading(true)
|
|
234
|
+
setUploadFailed(false)
|
|
235
|
+
try {
|
|
236
|
+
await uploadDocument(file, Object.keys(extra).length > 0 ? extra : undefined)
|
|
237
|
+
setReloadToken((token) => token + 1)
|
|
238
|
+
} catch {
|
|
239
|
+
setUploadFailed(true)
|
|
240
|
+
} finally {
|
|
241
|
+
setUploading(false)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
async function handleDownloadArchive(): Promise<void> {
|
|
228
246
|
if (!downloadArchive) return
|
|
229
247
|
setBusy(true)
|
|
@@ -304,8 +322,40 @@ export function DocumentsWorkspace({
|
|
|
304
322
|
{labels.clearFilters}
|
|
305
323
|
</button>
|
|
306
324
|
) : null}
|
|
325
|
+
|
|
326
|
+
{uploadDocument ? (
|
|
327
|
+
<>
|
|
328
|
+
<input
|
|
329
|
+
ref={fileInputRef}
|
|
330
|
+
type="file"
|
|
331
|
+
hidden
|
|
332
|
+
onChange={(event) => {
|
|
333
|
+
const file = event.target.files?.[0]
|
|
334
|
+
event.target.value = ''
|
|
335
|
+
if (file) void handleUpload(file)
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
338
|
+
<button
|
|
339
|
+
data-cv-tooltip={labels.upload}
|
|
340
|
+
aria-label={labels.upload}
|
|
341
|
+
type="button"
|
|
342
|
+
onClick={() => fileInputRef.current?.click()}
|
|
343
|
+
disabled={uploading}
|
|
344
|
+
className="cv-header-action ml-auto inline-flex items-center gap-1 disabled:opacity-40"
|
|
345
|
+
>
|
|
346
|
+
<Upload size={12} aria-hidden="true" />
|
|
347
|
+
{labels.upload}
|
|
348
|
+
</button>
|
|
349
|
+
</>
|
|
350
|
+
) : null}
|
|
307
351
|
</div>
|
|
308
352
|
|
|
353
|
+
{uploadFailed ? (
|
|
354
|
+
<p role="alert" className={cn('text-sm text-red-600 dark:text-red-400', classNames?.status)}>
|
|
355
|
+
{labels.uploadError}
|
|
356
|
+
</p>
|
|
357
|
+
) : null}
|
|
358
|
+
|
|
309
359
|
{canSelect ? (
|
|
310
360
|
<BulkActionBar
|
|
311
361
|
selectedCount={selectedIds.size}
|
package/src/documents/labels.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface DocumentsWorkspaceLabels {
|
|
|
28
28
|
readonly bulkDownloadZip: string
|
|
29
29
|
readonly bulkRemove: (count: number) => string
|
|
30
30
|
readonly bulkRemoveConfirm: (count: number) => string
|
|
31
|
+
readonly upload: string
|
|
32
|
+
readonly uploadError: string
|
|
31
33
|
readonly columnFilename: string
|
|
32
34
|
readonly columnContact: string
|
|
33
35
|
readonly columnType: string
|
|
@@ -70,6 +72,8 @@ export const DEFAULT_DOCUMENTS_WORKSPACE_LABELS: DocumentsWorkspaceLabels = {
|
|
|
70
72
|
bulkDownloadZip: 'Baixar em zip',
|
|
71
73
|
bulkRemove: (count) => `Excluir ${count}`,
|
|
72
74
|
bulkRemoveConfirm: (count) => `Excluir ${count} arquivo${count === 1 ? '' : 's'}?`,
|
|
75
|
+
upload: 'Enviar documento',
|
|
76
|
+
uploadError: 'Não foi possível enviar o arquivo.',
|
|
73
77
|
columnFilename: 'Arquivo',
|
|
74
78
|
columnContact: 'Contato',
|
|
75
79
|
columnType: 'Tipo',
|