From 4517c5022766b477d85059de44365abf1644462d Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:48:12 +0530 Subject: [PATCH] feat: support bulk select and delete for documents (#13907) --- .../captain/assistant/DocumentCard.vue | 38 ++++++- .../pageComponents/BulkDeleteDialog.vue | 16 ++- .../i18n/locale/en/integrations.json | 11 ++ .../dashboard/captain/documents/Index.vue | 100 +++++++++++++++++- .../dashboard/captain/responses/Index.vue | 2 +- .../dashboard/captain/responses/Pending.vue | 2 +- .../dashboard/store/captain/bulkActions.js | 21 ++-- .../dashboard/store/captain/document.js | 8 ++ .../captain/bulk_actions_controller.rb | 14 ++- .../captain/bulk_actions_controller_spec.rb | 30 ++++++ 10 files changed, 224 insertions(+), 18 deletions(-) diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue index bb5f03451..30ebfc448 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue @@ -12,6 +12,7 @@ import { import CardLayout from 'dashboard/components-next/CardLayout.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import Button from 'dashboard/components-next/button/Button.vue'; +import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue'; const props = defineProps({ id: { @@ -34,14 +35,34 @@ const props = defineProps({ type: Number, required: true, }, + isSelected: { + type: Boolean, + default: false, + }, + selectable: { + type: Boolean, + default: false, + }, + showSelectionControl: { + type: Boolean, + default: false, + }, + showMenu: { + type: Boolean, + default: true, + }, }); -const emit = defineEmits(['action']); +const emit = defineEmits(['action', 'select', 'hover']); const { checkPermissions } = usePolicy(); const { t } = useI18n(); const [showActionsDropdown, toggleDropdown] = useToggle(); +const modelValue = computed({ + get: () => props.isSelected, + set: () => emit('select', props.id), +}); const menuItems = computed(() => { const allOptions = [ @@ -79,12 +100,23 @@ const handleAction = ({ action, value }) => { diff --git a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue index 2144f6f05..e17b26999 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue @@ -316,7 +316,7 @@ onMounted(() => { v-if="bulkSelectedIds" ref="bulkDeleteDialog" :bulk-ids="bulkSelectedIds" - type="Responses" + type="AssistantResponse" @delete-success="onBulkDeleteSuccess" /> diff --git a/app/javascript/dashboard/routes/dashboard/captain/responses/Pending.vue b/app/javascript/dashboard/routes/dashboard/captain/responses/Pending.vue index b26658f9c..661b6ced7 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/responses/Pending.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/responses/Pending.vue @@ -361,7 +361,7 @@ onMounted(() => { v-if="bulkSelectedIds" ref="bulkDeleteDialog" :bulk-ids="bulkSelectedIds" - type="Responses" + type="AssistantResponse" @delete-success="onBulkDeleteSuccess" /> diff --git a/app/javascript/dashboard/store/captain/bulkActions.js b/app/javascript/dashboard/store/captain/bulkActions.js index 42ffcd294..436801092 100644 --- a/app/javascript/dashboard/store/captain/bulkActions.js +++ b/app/javascript/dashboard/store/captain/bulkActions.js @@ -25,17 +25,26 @@ export default createStore({ } }, - handleBulkDelete: async function handleBulkDelete({ dispatch }, ids) { + handleBulkDelete: async function handleBulkDelete( + { dispatch }, + { type = 'AssistantResponse', ids } + ) { const response = await dispatch('processBulkAction', { - type: 'AssistantResponse', + type, actionType: 'delete', ids, }); - // Update the response store after successful API call - await dispatch('captainResponses/removeBulkResponses', ids, { - root: true, - }); + if (type === 'AssistantResponse') { + // Update the response store after successful API call + await dispatch('captainResponses/removeBulkResponses', ids, { + root: true, + }); + } else if (type === 'AssistantDocument') { + await dispatch('captainDocuments/removeBulkRecords', ids, { + root: true, + }); + } return response; }, diff --git a/app/javascript/dashboard/store/captain/document.js b/app/javascript/dashboard/store/captain/document.js index f5766f827..76d0c9124 100644 --- a/app/javascript/dashboard/store/captain/document.js +++ b/app/javascript/dashboard/store/captain/document.js @@ -4,4 +4,12 @@ import { createStore } from '../storeFactory'; export default createStore({ name: 'CaptainDocument', API: CaptainDocumentAPI, + actions: mutations => ({ + removeBulkRecords({ commit, getters }, ids) { + const records = getters.getRecords.filter( + record => !ids.includes(record.id) + ); + commit(mutations.SET, records); + }, + }), }); diff --git a/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb index 3130a68cc..e7d02a887 100644 --- a/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::Bas before_action :validate_params before_action :type_matches? - MODEL_TYPE = ['AssistantResponse'].freeze + MODEL_TYPE = %w[AssistantResponse AssistantDocument].freeze def create @responses = process_bulk_action @@ -28,6 +28,8 @@ class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::Bas case params[:type] when 'AssistantResponse' handle_assistant_responses + when 'AssistantDocument' + handle_documents end end @@ -45,6 +47,16 @@ class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::Bas end end + def handle_documents + return [] unless params[:fields][:status] == 'delete' + + documents = Current.account.captain_documents.where(id: params[:ids]) + return [] unless documents.exists? + + documents.destroy_all + [] + end + def permitted_params params.permit(:type, ids: [], fields: [:status]) end diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb index 968649085..eaecdd89e 100644 --- a/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb @@ -14,6 +14,14 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do status: 'pending' ) end + let!(:documents) do + create_list( + :captain_document, + 2, + assistant: assistant, + account: account + ) + end def json_response JSON.parse(response.body, symbolize_names: true) @@ -98,6 +106,28 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do end end + context 'when deleting documents' do + let(:document_delete_params) do + { + type: 'AssistantDocument', + ids: documents.map(&:id), + fields: { status: 'delete' } + } + end + + it 'deletes the documents and returns an empty array' do + expect do + post "/api/v1/accounts/#{account.id}/captain/bulk_actions", + params: document_delete_params, + headers: admin.create_new_auth_token, + as: :json + end.to change(Captain::Document, :count).by(-2) + + expect(response).to have_http_status(:ok) + expect(json_response).to eq([]) + end + end + context 'with missing parameters' do let(:missing_params) do {