From 1280dae803d5437793b50eb4a7a1f611c6d3af20 Mon Sep 17 00:00:00 2001 From: israel Date: Thu, 19 Mar 2026 22:18:10 +0100 Subject: [PATCH 1/3] feat(platform): bulk archive/unarchive, read filter, and status banners Add bulk archive and unarchive operations for conversations with corresponding backend mutations, frontend hooks, and toast feedback. Redesign the inbox toolbar with icon-button bulk actions, a read/unread radio filter, and selected-count display. Add contextual action banners at the bottom of the conversation panel for closed, archived, and spam states with reopen/unarchive/not-spam buttons. Implement thread collapsing for long conversations and error state with retry. Update conversation list row layout with tighter spacing, unread dot indicator, and subject preview line. --- .../activate-conversations-empty-state.tsx | 3 +- .../components/conversation-panel.tsx | 333 +++++++++++++----- .../components/conversations-list.tsx | 54 +-- .../components/conversations-skeleton.tsx | 57 ++- .../components/conversations.tsx | 249 ++++++++++--- .../features/conversations/hooks/mutations.ts | 12 + .../conversations/hooks/use-bulk-actions.ts | 92 +++++ services/platform/convex/_generated/api.d.ts | 6 + .../bulk_archive_conversations.ts | 87 +++++ .../bulk_unarchive_conversations.ts | 86 +++++ .../platform/convex/conversations/helpers.ts | 2 + .../convex/conversations/mutations.ts | 20 ++ services/platform/messages/en.json | 87 +++-- 13 files changed, 878 insertions(+), 210 deletions(-) create mode 100644 services/platform/convex/conversations/bulk_archive_conversations.ts create mode 100644 services/platform/convex/conversations/bulk_unarchive_conversations.ts diff --git a/services/platform/app/features/conversations/components/activate-conversations-empty-state.tsx b/services/platform/app/features/conversations/components/activate-conversations-empty-state.tsx index 5c07c2eea..01729312c 100644 --- a/services/platform/app/features/conversations/components/activate-conversations-empty-state.tsx +++ b/services/platform/app/features/conversations/components/activate-conversations-empty-state.tsx @@ -1,6 +1,6 @@ 'use client'; -import { MessageSquare, Plus } from 'lucide-react'; +import { MessageSquare } from 'lucide-react'; import { LinkButton } from '@/app/components/ui/primitives/button'; import { Heading } from '@/app/components/ui/typography/heading'; @@ -28,7 +28,6 @@ export function ActivateConversationsEmptyState({ {t('activate.connectEmail')} diff --git a/services/platform/app/features/conversations/components/conversation-panel.tsx b/services/platform/app/features/conversations/components/conversation-panel.tsx index 8bc6a869c..3e8e75f47 100644 --- a/services/platform/app/features/conversations/components/conversation-panel.tsx +++ b/services/platform/app/features/conversations/components/conversation-panel.tsx @@ -1,20 +1,20 @@ 'use client'; -import { Loader2Icon, MessageSquareMoreIcon } from 'lucide-react'; -import { useEffect, useRef } from 'react'; +import { + AlertTriangleIcon, + Loader2Icon, + MessageSquareMoreIcon, + RefreshCwIcon, +} from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; import type { Id } from '@/convex/_generated/dataModel'; import { PanelFooter } from '@/app/components/layout/panel-footer'; -import { PanelHeader } from '@/app/components/layout/panel-header'; import { EmptyState } from '@/app/components/ui/feedback/empty-state'; import { Skeleton } from '@/app/components/ui/feedback/skeleton'; -import { - Center, - HStack, - Stack, - VStack, -} from '@/app/components/ui/layout/layout'; +import { Center, Stack } from '@/app/components/ui/layout/layout'; +import { Button } from '@/app/components/ui/primitives/button'; import { Text } from '@/app/components/ui/typography/text'; import { useThrottledScroll } from '@/app/hooks/use-throttled-scroll'; import { toast } from '@/app/hooks/use-toast'; @@ -26,10 +26,12 @@ import { useDownloadAttachments, useGenerateUploadUrl, useMarkAsRead, + useReopenConversation, useSendMessageViaIntegration, } from '../hooks/mutations'; import { useConversationWithMessages } from '../hooks/queries'; import { ConversationHeader } from './conversation-header'; +import { ConversationHeaderSkeleton } from './conversations-skeleton'; import { Message } from './message'; const MessageEditor = lazyComponent( @@ -46,6 +48,7 @@ const MessageEditor = lazyComponent( import { useFormatDate } from '@/app/hooks/use-format-date'; import { groupMessagesByDate } from '@/lib/utils/conversation/date-utils'; +import {cn} from '@/lib/utils/cn'; interface AttachedFile { id: string; @@ -56,25 +59,34 @@ interface AttachedFile { interface ConversationPanelProps { selectedConversationId: string | null; onSelectedConversationChange: (conversationId: string | null) => void; + status?: 'open' | 'closed' | 'archived' | 'spam'; } export function ConversationPanel({ selectedConversationId, onSelectedConversationChange, + status: tabStatus, }: ConversationPanelProps) { // Translations const { t: tConversations } = useT('conversations'); const { formatDateHeader } = useFormatDate(); - const { data: conversation, isLoading } = useConversationWithMessages( - selectedConversationId, - ); + const { + data: conversation, + isLoading, + isError, + refetch, + } = useConversationWithMessages(selectedConversationId); const { mutate: markAsRead } = useMarkAsRead(); const { mutateAsync: sendMessageViaIntegration } = useSendMessageViaIntegration(); const { mutateAsync: generateUploadUrl } = useGenerateUploadUrl(); const { mutate: downloadAttachments } = useDownloadAttachments(); + const { mutate: reopenConversation, isPending: isReopening } = + useReopenConversation(); + + const [isThreadCollapsed, setIsThreadCollapsed] = useState(true); const containerRef = useRef(null); const messageComposerRef = useRef(null); @@ -238,21 +250,36 @@ export function ConversationPanel({ ); } + if (isError) { + return ( +
+ +
+ {tConversations('panel.loadFailed')} + + {tConversations('panel.loadFailedDescription')} + +
+ +
+ ); + } + if (isLoading) { return (
- - - - - - - - - +
@@ -278,11 +305,20 @@ export function ConversationPanel({
- -
- -
-
+ {tabStatus && tabStatus !== 'open' ? ( + +
+ + +
+
+ ) : ( + +
+ +
+
+ )}
); } @@ -316,12 +352,17 @@ export function ConversationPanel({ const messageGroups = groupMessagesByDate(displayMessages); + const totalMessages = displayMessages.length; + const COLLAPSE_THRESHOLD = 4; + const showCollapse = totalMessages > COLLAPSE_THRESHOLD && isThreadCollapsed; + const collapsedHiddenCount = totalMessages - 2; + return (
- +
- +
{messageGroups.length === 0 ? (
{tConversations('panel.noMessages')}
) : ( - messageGroups.map((group) => ( -
- {/* Sticky Date Header */} -
-
-
- - {formatDateHeader(group.date)} - -
-
+ <> + {showCollapse && ( +
+
+ )} + {messageGroups.map((group, groupIndex) => { + const isLastGroup = groupIndex === messageGroups.length - 1; + const messagesToShow = + showCollapse && isLastGroup + ? group.messages.slice(-2) + : showCollapse && !isLastGroup + ? [] + : group.messages; + + if (messagesToShow.length === 0) return null; + + return ( +
+ {/* Sticky Date Header */} +
+
+
+ + {formatDateHeader(group.date)} + +
+
+
- {/* Messages for this date */} - - {group.messages.map((message) => ( - { - downloadAttachments( - { - messageId: toId<'conversationMessages'>(messageId), - }, - { - onError: (error) => { - console.error( - 'Failed to download attachments:', - error, - ); - toast({ - title: tConversations('panel.downloadFailed'), - variant: 'destructive', - }); - }, - }, - ); - }} - /> - ))} - -
- )) + {/* Messages for this date */} + + {messagesToShow.map((message) => ( + { + downloadAttachments( + { + messageId: + toId<'conversationMessages'>(messageId), + }, + { + onError: (error) => { + console.error( + 'Failed to download attachments:', + error, + ); + toast({ + title: tConversations('panel.downloadFailed'), + variant: 'destructive', + }); + }, + }, + ); + }} + /> + ))} + +
+ ); + })} + )}
- + {conversation.status === 'open' ? (
{ - // Convex will automatically update the conversation reactively - // Just call the parent callback to update the conversation list onSelectedConversationChange(null); }} pendingMessage={pendingMessage} hasMessageHistory={displayMessages.length > 0} />
- ) : ( -
- - {conversation.status === 'spam' - ? tConversations('panel.markedAsSpam') - : tConversations('panel.markedAsClosed')} + ) : conversation.status === 'closed' ? ( +
+ + {tConversations('panel.closedBanner')} +
- )} + ) : conversation.status === 'archived' ? ( +
+ + {tConversations('panel.archivedBanner')} + + +
+ ) : conversation.status === 'spam' ? ( +
+ + {tConversations('panel.spamBanner')} + + +
+ ) : null}
); diff --git a/services/platform/app/features/conversations/components/conversations-list.tsx b/services/platform/app/features/conversations/components/conversations-list.tsx index 03c8da753..2fd490b5a 100644 --- a/services/platform/app/features/conversations/components/conversations-list.tsx +++ b/services/platform/app/features/conversations/components/conversations-list.tsx @@ -229,7 +229,7 @@ const ConversationRow = memo(function ConversationRow({ - )} - {status === 'open' && ( - - )} - {status !== 'open' && ( - - )} + + {tConversations('bulk.selectedCount', { count: selectedCount })} + +
+ {status === 'open' && ( + + + + )} + {status === 'open' && ( + + + + )} + {status === 'open' && ( + + + + )} + {status === 'closed' && ( + + + + )} + {status === 'spam' && ( + + + + )} + {status === 'archived' ? ( + + + + ) : ( + + + + )} +
) : ( {isLoading ? ( - + ) : ( )}
diff --git a/services/platform/app/features/conversations/hooks/mutations.ts b/services/platform/app/features/conversations/hooks/mutations.ts index a36aa6449..ce6178e4a 100644 --- a/services/platform/app/features/conversations/hooks/mutations.ts +++ b/services/platform/app/features/conversations/hooks/mutations.ts @@ -11,6 +11,12 @@ export function useAddMessage() { ); } +export function useBulkArchiveConversations() { + return useConvexMutation( + api.conversations.mutations.bulkArchiveConversations, + ); +} + export function useBulkCloseConversations() { return useConvexMutation(api.conversations.mutations.bulkCloseConversations); } @@ -19,6 +25,12 @@ export function useBulkReopenConversations() { return useConvexMutation(api.conversations.mutations.bulkReopenConversations); } +export function useBulkUnarchiveConversations() { + return useConvexMutation( + api.conversations.mutations.bulkUnarchiveConversations, + ); +} + export function useSendMessageViaIntegration() { return useConvexMutation( api.conversations.mutations.sendMessageViaIntegration, diff --git a/services/platform/app/features/conversations/hooks/use-bulk-actions.ts b/services/platform/app/features/conversations/hooks/use-bulk-actions.ts index ff06423b7..b68282185 100644 --- a/services/platform/app/features/conversations/hooks/use-bulk-actions.ts +++ b/services/platform/app/features/conversations/hooks/use-bulk-actions.ts @@ -11,8 +11,10 @@ import type { SelectionState } from '../types/selection'; import { isAllSelection } from '../types/selection'; import { useAddMessage, + useBulkArchiveConversations, useBulkCloseConversations, useBulkReopenConversations, + useBulkUnarchiveConversations, } from './mutations'; function getSelectedConversationIds( @@ -39,8 +41,10 @@ export function useBulkActions({ }: UseBulkActionsOptions) { const { t: tConversations } = useT('conversations'); + const { mutateAsync: bulkArchive } = useBulkArchiveConversations(); const { mutateAsync: bulkResolve } = useBulkCloseConversations(); const { mutateAsync: bulkReopen } = useBulkReopenConversations(); + const { mutateAsync: bulkUnarchive } = useBulkUnarchiveConversations(); const { mutateAsync: addMessage } = useAddMessage(); const [isBulkProcessing, setIsBulkProcessing] = useState(false); @@ -204,6 +208,92 @@ export function useBulkActions({ onComplete, ]); + const handleBulkArchive = useCallback(async () => { + if (isBulkProcessing) return; + + setIsBulkProcessing(true); + + try { + const conversationIds = getSelectedConversationIds( + selectionState, + conversations, + ); + + const result = await bulkArchive({ + conversationIds: toIds<'conversations'>(conversationIds), + }); + + toast({ + title: tConversations('bulk.archived'), + description: tConversations('bulk.archivedDescription', { + successCount: result.successCount, + failedCount: result.failedCount, + }), + variant: result.successCount > 0 ? 'default' : 'destructive', + }); + + onComplete(); + } catch (error) { + console.error('Error archiving conversations:', error); + toast({ + title: tConversations('bulk.archiveFailed'), + variant: 'destructive', + }); + } finally { + setIsBulkProcessing(false); + } + }, [ + isBulkProcessing, + selectionState, + conversations, + bulkArchive, + tConversations, + onComplete, + ]); + + const handleBulkUnarchive = useCallback(async () => { + if (isBulkProcessing) return; + + setIsBulkProcessing(true); + + try { + const conversationIds = getSelectedConversationIds( + selectionState, + conversations, + ); + + const result = await bulkUnarchive({ + conversationIds: toIds<'conversations'>(conversationIds), + }); + + toast({ + title: tConversations('bulk.unarchived'), + description: tConversations('bulk.unarchivedDescription', { + successCount: result.successCount, + failedCount: result.failedCount, + }), + variant: result.successCount > 0 ? 'default' : 'destructive', + }); + + onComplete(); + } catch (error) { + console.error('Error unarchiving conversations:', error); + toast({ + title: tConversations('bulk.unarchiveFailed'), + variant: 'destructive', + }); + } finally { + setIsBulkProcessing(false); + } + }, [ + isBulkProcessing, + selectionState, + conversations, + bulkUnarchive, + tConversations, + onComplete, + ]); + return { isBulkProcessing, bulkSendDialog, @@ -212,5 +302,7 @@ export function useBulkActions({ handleSendMessages, handleBulkResolve, handleBulkReopen, + handleBulkArchive, + handleBulkUnarchive, }; } diff --git a/services/platform/convex/_generated/api.d.ts b/services/platform/convex/_generated/api.d.ts index a6a46269b..542ccd999 100644 --- a/services/platform/convex/_generated/api.d.ts +++ b/services/platform/convex/_generated/api.d.ts @@ -134,8 +134,10 @@ import type * as constants from "../constants.js"; import type * as conversations_actions from "../conversations/actions.js"; import type * as conversations_add_message_to_conversation from "../conversations/add_message_to_conversation.js"; import type * as conversations_build_threading_headers from "../conversations/build_threading_headers.js"; +import type * as conversations_bulk_archive_conversations from "../conversations/bulk_archive_conversations.js"; import type * as conversations_bulk_close_conversations from "../conversations/bulk_close_conversations.js"; import type * as conversations_bulk_reopen_conversations from "../conversations/bulk_reopen_conversations.js"; +import type * as conversations_bulk_unarchive_conversations from "../conversations/bulk_unarchive_conversations.js"; import type * as conversations_close_conversation from "../conversations/close_conversation.js"; import type * as conversations_create_conversation from "../conversations/create_conversation.js"; import type * as conversations_create_conversation_public from "../conversations/create_conversation_public.js"; @@ -260,6 +262,7 @@ import type * as folders_internal_mutations from "../folders/internal_mutations. import type * as folders_mutations from "../folders/mutations.js"; import type * as folders_queries from "../folders/queries.js"; import type * as http from "../http.js"; +import type * as images_http_actions from "../images/http_actions.js"; import type * as integrations_actions from "../integrations/actions.js"; import type * as integrations_build_test_secrets from "../integrations/build_test_secrets.js"; import type * as integrations_create_integration from "../integrations/create_integration.js"; @@ -989,8 +992,10 @@ declare const fullApi: ApiFromModules<{ "conversations/actions": typeof conversations_actions; "conversations/add_message_to_conversation": typeof conversations_add_message_to_conversation; "conversations/build_threading_headers": typeof conversations_build_threading_headers; + "conversations/bulk_archive_conversations": typeof conversations_bulk_archive_conversations; "conversations/bulk_close_conversations": typeof conversations_bulk_close_conversations; "conversations/bulk_reopen_conversations": typeof conversations_bulk_reopen_conversations; + "conversations/bulk_unarchive_conversations": typeof conversations_bulk_unarchive_conversations; "conversations/close_conversation": typeof conversations_close_conversation; "conversations/create_conversation": typeof conversations_create_conversation; "conversations/create_conversation_public": typeof conversations_create_conversation_public; @@ -1115,6 +1120,7 @@ declare const fullApi: ApiFromModules<{ "folders/mutations": typeof folders_mutations; "folders/queries": typeof folders_queries; http: typeof http; + "images/http_actions": typeof images_http_actions; "integrations/actions": typeof integrations_actions; "integrations/build_test_secrets": typeof integrations_build_test_secrets; "integrations/create_integration": typeof integrations_create_integration; diff --git a/services/platform/convex/conversations/bulk_archive_conversations.ts b/services/platform/convex/conversations/bulk_archive_conversations.ts new file mode 100644 index 000000000..d2735db9e --- /dev/null +++ b/services/platform/convex/conversations/bulk_archive_conversations.ts @@ -0,0 +1,87 @@ +import type { Id } from '../_generated/dataModel'; +import type { MutationCtx } from '../_generated/server'; +import type { BulkOperationResult } from './types'; + +import * as AuditLogHelpers from '../audit_logs/helpers'; +import { buildAuditContext } from '../lib/helpers/build_audit_context'; + +export async function bulkArchiveConversations( + ctx: MutationCtx, + args: { + conversationIds: Array>; + }, +): Promise { + const conversations = await Promise.all( + args.conversationIds.map((id) => ctx.db.get(id)), + ); + + const patches: Array<{ + id: Id<'conversations'>; + patch: Record; + }> = []; + const errors: string[] = []; + + for (let i = 0; i < args.conversationIds.length; i++) { + const conversationId = args.conversationIds[i]; + const conversation = conversations[i]; + + if (!conversation) { + errors.push(`Conversation ${conversationId} not found`); + continue; + } + + const existingMetadata = conversation.metadata ?? {}; + patches.push({ + id: conversationId, + patch: { + status: 'archived', + metadata: { + ...existingMetadata, + archived_at: new Date().toISOString(), + }, + }, + }); + } + + const results = await Promise.allSettled( + patches.map(({ id, patch }) => ctx.db.patch(id, patch)), + ); + + let successCount = 0; + for (let i = 0; i < results.length; i++) { + if (results[i].status === 'fulfilled') { + successCount++; + } else { + const result = results[i]; + const reason = result.status === 'rejected' ? result.reason : undefined; + errors.push( + `Failed to archive ${patches[i].id}: ${reason instanceof Error ? reason.message : 'Unknown error'}`, + ); + } + } + + const failedCount = args.conversationIds.length - successCount; + + const firstValidConversation = conversations.find((c) => c !== null); + if (firstValidConversation) { + await AuditLogHelpers.logSuccess( + ctx, + await buildAuditContext(ctx, firstValidConversation.organizationId), + 'bulk_archive_conversations', + 'data', + 'conversation', + undefined, + undefined, + undefined, + undefined, + { + conversationIds: args.conversationIds.map(String), + count: args.conversationIds.length, + successCount, + failedCount, + }, + ); + } + + return { successCount, failedCount, errors }; +} diff --git a/services/platform/convex/conversations/bulk_unarchive_conversations.ts b/services/platform/convex/conversations/bulk_unarchive_conversations.ts new file mode 100644 index 000000000..0e3e2812c --- /dev/null +++ b/services/platform/convex/conversations/bulk_unarchive_conversations.ts @@ -0,0 +1,86 @@ +import type { Id } from '../_generated/dataModel'; +import type { MutationCtx } from '../_generated/server'; +import type { BulkOperationResult } from './types'; + +import * as AuditLogHelpers from '../audit_logs/helpers'; +import { buildAuditContext } from '../lib/helpers/build_audit_context'; + +export async function bulkUnarchiveConversations( + ctx: MutationCtx, + args: { + conversationIds: Array>; + }, +): Promise { + const conversations = await Promise.all( + args.conversationIds.map((id) => ctx.db.get(id)), + ); + + const patches: Array<{ + id: Id<'conversations'>; + patch: Record; + }> = []; + const errors: string[] = []; + + for (let i = 0; i < args.conversationIds.length; i++) { + const conversationId = args.conversationIds[i]; + const conversation = conversations[i]; + + if (!conversation) { + errors.push(`Conversation ${conversationId} not found`); + continue; + } + + const metadata = conversation.metadata ?? {}; + const { archived_at: _, ...restMetadata } = metadata; + + patches.push({ + id: conversationId, + patch: { + status: 'closed', + metadata: restMetadata, + }, + }); + } + + const results = await Promise.allSettled( + patches.map(({ id, patch }) => ctx.db.patch(id, patch)), + ); + + let successCount = 0; + for (let i = 0; i < results.length; i++) { + if (results[i].status === 'fulfilled') { + successCount++; + } else { + const result = results[i]; + const reason = result.status === 'rejected' ? result.reason : undefined; + errors.push( + `Failed to unarchive ${patches[i].id}: ${reason instanceof Error ? reason.message : 'Unknown error'}`, + ); + } + } + + const failedCount = args.conversationIds.length - successCount; + + const firstValidConversation = conversations.find((c) => c !== null); + if (firstValidConversation) { + await AuditLogHelpers.logSuccess( + ctx, + await buildAuditContext(ctx, firstValidConversation.organizationId), + 'bulk_unarchive_conversations', + 'data', + 'conversation', + undefined, + undefined, + undefined, + undefined, + { + conversationIds: args.conversationIds.map(String), + count: args.conversationIds.length, + successCount, + failedCount, + }, + ); + } + + return { successCount, failedCount, errors }; +} diff --git a/services/platform/convex/conversations/helpers.ts b/services/platform/convex/conversations/helpers.ts index 30434f1fe..43f2acd4f 100644 --- a/services/platform/convex/conversations/helpers.ts +++ b/services/platform/convex/conversations/helpers.ts @@ -20,8 +20,10 @@ export * from './close_conversation'; export * from './reopen_conversation'; export * from './mark_conversation_as_spam'; export * from './mark_conversation_as_read'; +export * from './bulk_archive_conversations'; export * from './bulk_close_conversations'; export * from './bulk_reopen_conversations'; +export * from './bulk_unarchive_conversations'; export * from './transform_conversation'; export * from './send_message_via_integration'; export * from './query_conversation_messages'; diff --git a/services/platform/convex/conversations/mutations.ts b/services/platform/convex/conversations/mutations.ts index e03ccbd23..f01cca0e6 100644 --- a/services/platform/convex/conversations/mutations.ts +++ b/services/platform/convex/conversations/mutations.ts @@ -119,6 +119,16 @@ export const markConversationAsRead = mutationWithRLS({ }, }); +export const bulkArchiveConversations = mutationWithRLS({ + args: { + conversationIds: v.array(v.id('conversations')), + }, + returns: bulkOperationResultValidator, + handler: async (ctx, args) => { + return await ConversationsHelpers.bulkArchiveConversations(ctx, args); + }, +}); + export const bulkCloseConversations = mutationWithRLS({ args: { conversationIds: v.array(v.id('conversations')), @@ -140,6 +150,16 @@ export const bulkReopenConversations = mutationWithRLS({ }, }); +export const bulkUnarchiveConversations = mutationWithRLS({ + args: { + conversationIds: v.array(v.id('conversations')), + }, + returns: bulkOperationResultValidator, + handler: async (ctx, args) => { + return await ConversationsHelpers.bulkUnarchiveConversations(ctx, args); + }, +}); + export const downloadAttachments = mutationWithRLS({ args: { messageId: v.id('conversationMessages'), diff --git a/services/platform/messages/en.json b/services/platform/messages/en.json index 57ace356f..c24c41e09 100644 --- a/services/platform/messages/en.json +++ b/services/platform/messages/en.json @@ -1217,7 +1217,6 @@ "testAndCreate": "Test & create provider", "useApiSending": "Use API sending (recommended - no port blocking)", "useApiSendingShort": "Use API sending", - "setAsDefault": "Set as default", "apiSending": "API sending", "providerGmail": "Gmail", "providerOutlook": "Outlook", @@ -1701,15 +1700,25 @@ "descriptionPlaceholder": "Describe what this automation does...", "creating": "Creating...", "continue": "Continue", - "tabBlank": "Blank", - "tabTemplate": "From template", "initialPrompt": "I just created a new automation called \"{name}\". Here is what it should do:\n\n{description}\n\nPlease help me build the workflow steps for this automation." }, "templates": { - "description": "Choose a pre-built template to get started quickly.", - "noTemplates": "No templates available for this integration.", + "description": "Choose a template to get started with pre-configured steps.", "fetchError": "Failed to load template. Please try again.", - "fetching": "Loading template..." + "fetching": "Loading template...", + "noTemplates": "No templates available for this integration.", + "newAutomation": "New automation", + "createNewTemplate": "Create a new automation template", + "fillDetails": "Fill in the details for your new workflow.", + "automationTemplates": "Automation templates", + "manageExisting": "Manage existing automation templates", + "loading": "Loading automations...", + "noAutomationsFound": "No automations found. Create your first automation above.", + "configJson": "Config (JSON)", + "configDescription": "Automation configuration settings", + "noDescription": "No description", + "deleteTitle": "Delete automation?", + "deleteDescription": "This action cannot be undone. This will permanently delete the automation template and its steps." }, "editDialog": { "title": "Edit automation", @@ -1847,24 +1856,6 @@ "validation": { "duplicateName": "An automation with this name already exists" }, - "templates": { - "description": "Choose a template to get started with pre-configured steps.", - "fetchError": "Failed to load template. Please try again.", - "fetching": "Loading template...", - "noTemplates": "No templates available for this integration.", - "newAutomation": "New automation", - "createNewTemplate": "Create a new automation template", - "fillDetails": "Fill in the details for your new workflow.", - "automationTemplates": "Automation templates", - "manageExisting": "Manage existing automation templates", - "loading": "Loading automations...", - "noAutomationsFound": "No automations found. Create your first automation above.", - "configJson": "Config (JSON)", - "configDescription": "Automation configuration settings", - "noDescription": "No description", - "deleteTitle": "Delete automation?", - "deleteDescription": "This action cannot be undone. This will permanently delete the automation template and its steps." - }, "deleteAutomation": { "title": "Delete automation", "description": "Are you sure you want to delete \"{name}\"? This action cannot be undone." @@ -1989,9 +1980,20 @@ "resolveFailed": "Failed to resolve conversations", "reopened": "Conversations reopened", "reopenedDescription": "Successfully reopened {successCount} conversations{failedCount, plural, =0 {} other {, {failedCount} failed}}", - "reopenFailed": "Failed to reopen conversations" + "reopenFailed": "Failed to reopen conversations", + "selectedCount": "{count, plural, one {# selected} other {# selected}}", + "archive": "Archive", + "unarchive": "Unarchive", + "archived": "Conversations archived", + "archivedDescription": "Successfully archived {successCount} conversations{failedCount, plural, =0 {} other {, {failedCount} failed}}", + "archiveFailed": "Failed to archive conversations", + "unarchived": "Conversations unarchived", + "unarchivedDescription": "Successfully unarchived {successCount} conversations{failedCount, plural, =0 {} other {, {failedCount} failed}}", + "unarchiveFailed": "Failed to unarchive conversations", + "markSpam": "Mark as spam" }, "header": { + "moreActions": "More actions", "customerInfo": "Customer info", "closeConversation": "Close conversation", "closing": "Closing...", @@ -2015,7 +2017,8 @@ "backToEditor": "Back to editor", "improving": "Improving...", "improveWithAi": "Improve with AI", - "generateImprovement": "Generate improvement" + "generateImprovement": "Generate improvement", + "replyImproved": "Reply improved" }, "attachment": { "download": "Download", @@ -2034,7 +2037,16 @@ "invalidFileAttachment": "Invalid file attachment", "defaultSubject": "Re: Conversation", "customerEmailNotFound": "Cannot send email: customer email not found", - "replySubjectPrefix": "Re: {subject}" + "replySubjectPrefix": "Re: {subject}", + "loadFailed": "Failed to load conversation", + "loadFailedDescription": "Something went wrong while loading this conversation.", + "tryAgain": "Try again", + "closedBanner": "This conversation was closed.", + "archivedBanner": "This conversation was archived.", + "spamBanner": "This conversation was marked as spam.", + "unarchive": "Unarchive", + "notSpam": "Not spam", + "showEarlierMessages": "Show {count} earlier {count, plural, one {message} other {messages}}" }, "filters": { "title": "Filters", @@ -2067,6 +2079,12 @@ "title": "Activate conversations", "description": "Connect your email to get started with conversations", "connectEmail": "Connect email" + }, + "filter": { + "label": "Filter by read status", + "all": "All", + "read": "Read", + "unread": "Unread" } }, "workflowRunApproval": { @@ -2076,7 +2094,7 @@ "rejectTooltip": "Cancel workflow execution", "statusApprovedSuccess": "Workflow execution started successfully.", "statusApprovedFailed": "Workflow execution was approved but failed to start.", - "statusRejected": "Workflow execution was cancelled.", + "statusRejected": "Rejected", "showParameters": "Show parameters", "hideParameters": "Hide parameters", "executionRunning": "Running...", @@ -2091,8 +2109,7 @@ "stopExecution": "Stop", "stopTooltip": "Stop workflow execution", "statusPending": "Pending", - "statusApproved": "Approved", - "statusRejected": "Rejected" + "statusApproved": "Approved" }, "documentWriteApproval": { "cardTitle": "Save to documents", @@ -2138,7 +2155,7 @@ "deleteConfirmation": "Are you sure you want to delete {title}?", "deleteArchiveMessage": "This chat will be archived and won't appear in your chat history.", "deleteFailed": "Failed to delete chat", - "welcome": "What are you working on?", + "welcomeSuffix": "here, what are we working on?", "suggestions": { "admin": [ "Draft a project proposal for Q4", @@ -2906,7 +2923,7 @@ }, "viewPages": "View pages", "indexed": "Indexed", - "indexedTooltip": "{percentage}\u202F% - {crawled} of {total} pages", + "indexedTooltip": "{percentage} % - {crawled} of {total} pages", "pageCount": "{count} pages", "pageCountOne": "{count} page", "pageCountProgress": "{crawled}/{total} pages", @@ -3026,7 +3043,7 @@ }, "customAgents": { "title": "Custom agents", - "description": "Manage custom AI agents for your organization." + "description": "Create and manage AI agents for customer service." }, "integrations": { "title": "Integrations", @@ -3064,10 +3081,6 @@ "title": "Knowledge", "description": "Manage your knowledge base for AI agents." }, - "customAgents": { - "title": "Custom agents", - "description": "Create and manage AI agents for customer service." - }, "customAgent": { "title": "Custom agent", "description": "Configure and test your custom AI agent." From 03a86671e5f6bcfc6cf7bf126520e9c0279c5a2d Mon Sep 17 00:00:00 2001 From: israel Date: Thu, 19 Mar 2026 22:42:56 +0100 Subject: [PATCH 2/3] fix(platform): accessibility and bulk spam button fixes Add aria-label and role to unread dot for screen reader support. Disable bulk mark-spam button since no handler is implemented yet. Make filter dropdown trigger a focusable button for keyboard access. --- .../conversations/components/conversations-list.tsx | 6 +++++- .../features/conversations/components/conversations.tsx | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/platform/app/features/conversations/components/conversations-list.tsx b/services/platform/app/features/conversations/components/conversations-list.tsx index 2fd490b5a..a4d59805a 100644 --- a/services/platform/app/features/conversations/components/conversations-list.tsx +++ b/services/platform/app/features/conversations/components/conversations-list.tsx @@ -263,7 +263,11 @@ const ConversationRow = memo(function ConversationRow({ 'Unknown'} {conversation.unread_count > 0 && ( - + )}
{/* Prevent checkbox clicks from opening the dropdown */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} @@ -211,7 +213,7 @@ export function Conversations({ /> - + } items={[ [ @@ -279,7 +281,7 @@ export function Conversations({