@adatechnology/conversations-ui 0.1.0-rc.28 → 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.
@@ -1966,9 +1966,9 @@ function ConversationDocumentsPanel({
1966
1966
  }
1967
1967
 
1968
1968
  // src/DocumentsLibrary.tsx
1969
- import { useEffect as useEffect4, useState as useState11 } from "react";
1970
- import { ArrowUpDown as ArrowUpDown2, Bot as Bot2, Download as Download2, Eye as Eye2, MessageSquare, Users as Users2 } from "lucide-react";
1971
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1969
+ import { useEffect as useEffect4, useRef as useRef5, useState as useState11 } from "react";
1970
+ import { ArrowUpDown as ArrowUpDown2, Bot as Bot2, Download as Download2, Eye as Eye2, MessageSquare, Upload, Users as Users2 } from "lucide-react";
1971
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1972
1972
  var DEFAULT_DOCUMENTS_LIBRARY_LABELS = {
1973
1973
  title: "Documentos",
1974
1974
  searchPlaceholder: "Buscar por nome do arquivo ou telefone",
@@ -1985,6 +1985,8 @@ var DEFAULT_DOCUMENTS_LIBRARY_LABELS = {
1985
1985
  sortMostRecent: "Mais recentes",
1986
1986
  sortOldest: "Mais antigos",
1987
1987
  clearFilters: "Limpar filtros",
1988
+ upload: "Enviar documento",
1989
+ uploadError: "N\xE3o foi poss\xEDvel enviar o arquivo.",
1988
1990
  total: (count) => `${count} arquivo${count === 1 ? "" : "s"}`,
1989
1991
  page: (current, last) => `${current} / ${last}`
1990
1992
  };
@@ -2007,9 +2009,14 @@ function DocumentsLibrary({
2007
2009
  const [total, setTotal] = useState11(0);
2008
2010
  const [loading, setLoading] = useState11(false);
2009
2011
  const [failed, setFailed] = useState11(false);
2012
+ const [uploading, setUploading] = useState11(false);
2013
+ const [uploadFailed, setUploadFailed] = useState11(false);
2014
+ const [reloadToken, setReloadToken] = useState11(0);
2015
+ const fileInputRef = useRef5(null);
2010
2016
  const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
2011
2017
  const lastPage = Math.max(1, Math.ceil(total / perPage));
2012
2018
  const fetchAll = context?.api.getAllDocuments;
2019
+ const uploadDocument = context?.api.uploadDocument;
2013
2020
  useEffect4(() => {
2014
2021
  if (!fetchAll) return;
2015
2022
  let active = true;
@@ -2033,7 +2040,7 @@ function DocumentsLibrary({
2033
2040
  return () => {
2034
2041
  active = false;
2035
2042
  };
2036
- }, [fetchAll, search, sourceFilter, sortDirection, page, perPage]);
2043
+ }, [fetchAll, search, sourceFilter, sortDirection, page, perPage, reloadToken]);
2037
2044
  function applyFilter(change) {
2038
2045
  change();
2039
2046
  setPage(1);
@@ -2042,6 +2049,19 @@ function DocumentsLibrary({
2042
2049
  const url = await context?.api.getDocumentUrl(uploadId, disposition);
2043
2050
  if (url) window.open(url, "_blank", "noopener,noreferrer");
2044
2051
  }
2052
+ async function handleUpload(file) {
2053
+ if (!uploadDocument) return;
2054
+ setUploading(true);
2055
+ setUploadFailed(false);
2056
+ try {
2057
+ await uploadDocument(file);
2058
+ setReloadToken((token) => token + 1);
2059
+ } catch {
2060
+ setUploadFailed(true);
2061
+ } finally {
2062
+ setUploading(false);
2063
+ }
2064
+ }
2045
2065
  if (!fetchAll) return null;
2046
2066
  return /* @__PURE__ */ jsxs12("div", { className: cn("space-y-3", classNames?.root, className), children: [
2047
2067
  /* @__PURE__ */ jsx17("h2", { className: cn("text-lg font-semibold", classNames?.title), children: labels.title }),
@@ -2099,10 +2119,41 @@ function DocumentsLibrary({
2099
2119
  className: cn("cv-header-action", classNames?.clearButton),
2100
2120
  children: labels.clearFilters
2101
2121
  }
2102
- ) : null
2122
+ ) : null,
2123
+ uploadDocument ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
2124
+ /* @__PURE__ */ jsx17(
2125
+ "input",
2126
+ {
2127
+ ref: fileInputRef,
2128
+ type: "file",
2129
+ hidden: true,
2130
+ onChange: (event) => {
2131
+ const file = event.target.files?.[0];
2132
+ event.target.value = "";
2133
+ if (file) void handleUpload(file);
2134
+ }
2135
+ }
2136
+ ),
2137
+ /* @__PURE__ */ jsxs12(
2138
+ "button",
2139
+ {
2140
+ "data-cv-tooltip": labels.upload,
2141
+ "aria-label": labels.upload,
2142
+ type: "button",
2143
+ onClick: () => fileInputRef.current?.click(),
2144
+ disabled: uploading,
2145
+ className: "cv-header-action ml-auto inline-flex items-center gap-1 disabled:opacity-40",
2146
+ children: [
2147
+ /* @__PURE__ */ jsx17(Upload, { size: 14, "aria-hidden": "true" }),
2148
+ labels.upload
2149
+ ]
2150
+ }
2151
+ )
2152
+ ] }) : null
2103
2153
  ] }),
2104
2154
  loading ? /* @__PURE__ */ jsx17("p", { className: cn("text-sm text-gray-500", classNames?.status), children: labels.loading }) : null,
2105
2155
  failed ? /* @__PURE__ */ jsx17("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
2156
+ uploadFailed ? /* @__PURE__ */ jsx17("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.uploadError }) : null,
2106
2157
  !loading && !failed && documents.length === 0 ? /* @__PURE__ */ jsx17("p", { className: cn("text-sm text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
2107
2158
  /* @__PURE__ */ jsx17("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
2108
2159
  const isFromCustomer = !TEAM_SOURCES2.has(document2.source);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import react__default, { ReactNode, CSSProperties, UIEvent, FormEvent, RefObject } from 'react';
3
- import { G as MessagePayload, N as ResolveMediaUrl, z as InteractiveSelection, J as MessageTranscription, x as InteractivePayload, r as ConversationsFeatures, o as ConversationSummary, j as ConversationChannel, L as ListConversationsParams, q as ConversationsApi, S as SSEProvider, T as TranscriptionMode, B as ListDocumentsParams, k as ConversationDocument, p as ConversationTemplate, f as ChannelFilter, g as ChannelFilterOption } from './types-BfINicc-.js';
4
- export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, C as CHANNEL_CAPABILITIES, c as CHANNEL_FILTER_ALL, d as CONVERSATION_CHANNEL, e as ChannelCapabilities, h as CompanyDocument, i as CompanyDocumentPage, l as ConversationDocumentPage, m as ConversationEventSource, n as ConversationPage, s as ConversationsTheme, t as ConversationsUIConfig, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, u as DEFAULT_CONVERSATION_CHANNEL, v as DEFAULT_MAX_RECORDING_MILLISECONDS, F as FormatContactHandleParams, H as HANDLE_KIND, w as HandleKind, I as InteractiveOption, y as InteractiveSection, M as MediaRenderer, E as MediaRendererProps, R as REOPEN_MECHANISM, K as ReopenMechanism, O as TranscriptionStatus, P as capabilitiesOf, Q as channelFiltersFor, U as contactFlag, V as formatContactHandle } from './types-BfINicc-.js';
3
+ import { G as MessagePayload, N as ResolveMediaUrl, z as InteractiveSelection, J as MessageTranscription, x as InteractivePayload, r as ConversationsFeatures, o as ConversationSummary, j as ConversationChannel, L as ListConversationsParams, q as ConversationsApi, S as SSEProvider, T as TranscriptionMode, B as ListDocumentsParams, k as ConversationDocument, p as ConversationTemplate, f as ChannelFilter, g as ChannelFilterOption } from './types-De5aN-E_.js';
4
+ export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, C as CHANNEL_CAPABILITIES, c as CHANNEL_FILTER_ALL, d as CONVERSATION_CHANNEL, e as ChannelCapabilities, h as CompanyDocument, i as CompanyDocumentPage, l as ConversationDocumentPage, m as ConversationEventSource, n as ConversationPage, s as ConversationsTheme, t as ConversationsUIConfig, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, u as DEFAULT_CONVERSATION_CHANNEL, v as DEFAULT_MAX_RECORDING_MILLISECONDS, F as FormatContactHandleParams, H as HANDLE_KIND, w as HandleKind, I as InteractiveOption, y as InteractiveSection, M as MediaRenderer, E as MediaRendererProps, R as REOPEN_MECHANISM, K as ReopenMechanism, O as TranscriptionStatus, P as capabilitiesOf, Q as channelFiltersFor, U as contactFlag, V as formatContactHandle } from './types-De5aN-E_.js';
5
5
 
6
6
  interface MessageBubbleProps {
7
7
  message: MessagePayload;
@@ -727,6 +727,8 @@ interface DocumentsLibraryLabels {
727
727
  sortMostRecent: string;
728
728
  sortOldest: string;
729
729
  clearFilters: string;
730
+ upload: string;
731
+ uploadError: string;
730
732
  total: (count: number) => string;
731
733
  page: (current: number, last: number) => string;
732
734
  }
@@ -835,6 +837,8 @@ interface DocumentsWorkspaceLabels {
835
837
  readonly bulkDownloadZip: string;
836
838
  readonly bulkRemove: (count: number) => string;
837
839
  readonly bulkRemoveConfirm: (count: number) => string;
840
+ readonly upload: string;
841
+ readonly uploadError: string;
838
842
  readonly columnFilename: string;
839
843
  readonly columnContact: string;
840
844
  readonly columnType: string;
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ import {
57
57
  useConversationDocuments,
58
58
  useConversationLocales,
59
59
  useConversations
60
- } from "./chunk-UZJBYD5O.js";
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 useRef5, useCallback as useCallback5 } from "react";
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 = useRef5(/* @__PURE__ */ new Map());
2530
- const notifiedIds = useRef5(/* @__PURE__ */ new Set());
2531
- const isPollingRef = useRef5(false);
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 useRef6, useState as useState15 } from "react";
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 = useRef6(null);
3768
+ const containerRef = useRef7(null);
3719
3769
  const [isAwayFromBottom, setIsAwayFromBottom] = useState15(false);
3720
- const isAwayFromBottomRef = useRef6(false);
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 = useRef6(void 0);
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 useRef7 } from "react";
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 = useRef7(onEvent);
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 = useRef7(onEvent);
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 useRef8, useState as useState17 } from "react";
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 = useRef8(false);
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 useRef9, useState as useState18 } from "react";
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 = useRef9(void 0);
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);
@@ -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-BfINicc-.js';
2
- export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS } from '../types-BfINicc-.js';
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';
@@ -8,7 +8,7 @@ import {
8
8
  DocumentsLibrary,
9
9
  MessageBubble,
10
10
  MessageComposer
11
- } from "../chunk-UZJBYD5O.js";
11
+ } from "../chunk-GY472G6E.js";
12
12
  import "../chunk-WCBDXZ3X.js";
13
13
 
14
14
  // src/preview/previewStore.ts
@@ -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.28",
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"
@@ -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}
@@ -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}
@@ -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',
@@ -145,6 +145,17 @@ export interface ConversationsApi {
145
145
  * `downloadDocumentsArchive`, que é por conversa. Ausente, a seleção em lote não oferece o botão.
146
146
  */
147
147
  downloadDocumentsArchiveByIds?(uploadIds: readonly string[]): Promise<Blob>
148
+ /**
149
+ * Envia um arquivo avulso direto pra biblioteca, fora do fluxo de uma conversa. **Opcional por
150
+ * capacidade:** cada host tem seu próprio contrato de upload (base64, multipart, presigned URL) —
151
+ * o pacote não escolhe um formato de payload, só entrega o `File` do input e deixa o host montar
152
+ * a chamada do jeito que seu backend espera. Ausente, a tela de biblioteca não desenha o botão de
153
+ * enviar, em vez de oferecer uma ação que sempre falha.
154
+ *
155
+ * `extra` é o mesmo vocabulário livre do produto que já viaja em `renderFilters` — cliente,
156
+ * unidade, campanha — pra associar o arquivo enviado ao contexto que a tela estava filtrando.
157
+ */
158
+ uploadDocument?(file: File, extra?: Readonly<Record<string, string | number>>): Promise<ConversationDocument>
148
159
  getMediaProxyUrl(mediaId: string): Promise<{ mimeType: string; data: string }>
149
160
 
150
161
  /**