diff --git a/frontend/apps/main/src/features/conversation/list/ConversationList.vue b/frontend/apps/main/src/features/conversation/list/ConversationList.vue index 40821ba5..6b665cae 100644 --- a/frontend/apps/main/src/features/conversation/list/ConversationList.vue +++ b/frontend/apps/main/src/features/conversation/list/ConversationList.vue @@ -6,8 +6,113 @@ {{ title }} - -
+ + + + +
@@ -149,26 +254,61 @@ diff --git a/frontend/apps/main/src/stores/conversation.js b/frontend/apps/main/src/stores/conversation.js index a1c2f96b..860c68ca 100644 --- a/frontend/apps/main/src/stores/conversation.js +++ b/frontend/apps/main/src/stores/conversation.js @@ -25,6 +25,9 @@ export const useConversationStore = defineStore('conversation', () => { const macros = ref({}) const drafts = ref(new Map()) + // Bulk selection state + const selectedUUIDs = ref(new Set()) + // Options for select fields const priorityOptions = computed(() => { return priorities.value.map(p => ({ label: p.name, value: p.id })) @@ -40,6 +43,51 @@ export const useConversationStore = defineStore('conversation', () => { })) ) + // Bulk selection methods + let lastClickedUUID = null + + const selectedCount = computed(() => selectedUUIDs.value.size) + const allSelected = computed(() => { + const list = conversationsList.value + return list.length > 0 && selectedUUIDs.value.size === list.length + }) + + function toggleSelect (uuid, shiftKey = false) { + const next = new Set(selectedUUIDs.value) + + if (shiftKey && lastClickedUUID && lastClickedUUID !== uuid) { + const list = conversationsList.value + const lastIdx = list.findIndex(c => c.uuid === lastClickedUUID) + const curIdx = list.findIndex(c => c.uuid === uuid) + if (lastIdx !== -1 && curIdx !== -1) { + const start = Math.min(lastIdx, curIdx) + const end = Math.max(lastIdx, curIdx) + for (let i = start; i <= end; i++) { + next.add(list[i].uuid) + } + } + } else { + if (next.has(uuid)) next.delete(uuid) + else next.add(uuid) + } + + lastClickedUUID = uuid + selectedUUIDs.value = next + } + + function selectAll () { + selectedUUIDs.value = new Set(conversationsList.value.map(c => c.uuid)) + } + + function clearSelection () { + selectedUUIDs.value = new Set() + lastClickedUUID = null + } + + function isSelected (uuid) { + return selectedUUIDs.value.has(uuid) + } + // TODO: Move to constants. const sortFieldMap = { oldest: { @@ -798,6 +846,7 @@ export const useConversationStore = defineStore('conversation', () => { conversations.data = [] conversations.page = 1 seenConversationUUIDs = new Map() + clearSelection() } /** Macros set for new conversation or an open conversation **/ @@ -975,6 +1024,13 @@ export const useConversationStore = defineStore('conversation', () => { hasDraft, addPendingMessage, replacePendingMessage, - removePendingMessage + removePendingMessage, + selectedUUIDs, + selectedCount, + allSelected, + toggleSelect, + selectAll, + clearSelection, + isSelected } }) diff --git a/i18n/en.json b/i18n/en.json index 58ae7c04..d6cb5a05 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -467,6 +467,14 @@ "contextLink.urlTemplateHelp": "{'{{token}}'} is a base64-encoded AES-256-GCM encrypted blob containing all contact and agent fields (requires secret). Individual variables like {'{{email}}'}, {'{{phone}}'}, {'{{external_user_id}}'}, {'{{contact_id}}'}, {'{{first_name}}'}, {'{{last_name}}'}, {'{{conversation_uuid}}'} are passed as plain text.", "conversation.agentAssigned": "Agent assigned", "conversation.allLoaded": "All conversations loaded", + "conversation.bulkActions.assign": "Assign", + "conversation.bulkActions.clearSelection": "Clear selection", + "conversation.bulkActions.failedToast": "Updated {success}, failed {failed} of {total} conversations", + "conversation.bulkActions.selectAll": "Select all conversations", + "conversation.bulkActions.selectConversation": "Select conversation", + "conversation.bulkActions.selected": "No conversations selected | {count} selected | {count} selected", + "conversation.bulkActions.successToast": "Updated {count} conversation | Updated {count} conversations", + "conversation.bulkActions.toolbar": "Bulk actions toolbar", "conversation.couldNotFetch": "Could not fetch conversations", "conversation.hideQuotedText": "Hide quoted text", "conversation.mentions": "Mentions",