diff --git a/app/controllers/api/v1/accounts/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/bulk_actions_controller.rb index 1b8babbc9..222c66714 100644 --- a/app/controllers/api/v1/accounts/bulk_actions_controller.rb +++ b/app/controllers/api/v1/accounts/bulk_actions_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll enqueue_conversation_job head :ok when 'Contact' + check_authorization_for_contact_action enqueue_contact_job head :ok else @@ -34,14 +35,34 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll ) end + def delete_contact_action? + params[:action_name] == 'delete' + end + + def check_authorization_for_contact_action + authorize(Contact, :destroy?) if delete_contact_action? + end + def conversation_params - params.permit(:type, :snoozed_until, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) + # TODO: Align conversation payloads with the `{ action_name, action_attributes }` + # and then remove this method in favor of a common params method. + base = params.permit( + :snoozed_until, + fields: [:status, :assignee_id, :team_id] + ) + append_common_bulk_attributes(base) end def contact_params - params.require(:ids) - permitted = params.permit(:type, ids: [], labels: [add: []]) - permitted[:ids] = permitted[:ids].map(&:to_i) if permitted[:ids].present? - permitted + # TODO: remove this method in favor of a common params method. + # once legacy conversation payloads are migrated. + append_common_bulk_attributes({}) + end + + def append_common_bulk_attributes(base_params) + # NOTE: Conversation payloads historically diverged per action. Going forward we + # want all objects to share a common contract: `{ action_name, action_attributes }` + common = params.permit(:type, :action_name, ids: [], labels: [add: [], remove: []]) + base_params.merge(common) end end diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue index 47b779b61..041f06410 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue @@ -5,6 +5,7 @@ import { useToggle } from '@vueuse/core'; import Button from 'dashboard/components-next/button/Button.vue'; import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue'; +import Policy from 'dashboard/components/policy.vue'; defineProps({ selectedContact: { @@ -24,42 +25,44 @@ const openConfirmDeleteContactDialog = () => { diff --git a/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue b/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue index aec21a976..b7014c42f 100644 --- a/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue +++ b/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue @@ -10,6 +10,7 @@ import Button from 'dashboard/components-next/button/Button.vue'; import ContactLabels from 'dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue'; import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue'; import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue'; +import Policy from 'dashboard/components/policy.vue'; const props = defineProps({ selectedContact: { @@ -174,27 +175,29 @@ const handleAvatarDelete = async () => { @click="updateContact" /> -
-
-
- {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT') }} -
- - {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT_DESCRIPTION') }} - + +
+
+
+ {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT') }} +
+ + {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT_DESCRIPTION') }} + +
+
-
- +
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 9b87da6b2..a711a05af 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -580,7 +580,18 @@ "NO_LABELS_FOUND": "No labels available yet.", "SELECTED_COUNT": "{count} selected", "CLEAR_SELECTION": "Clear selection", - "SELECT_ALL": "Select all ({count})" + "SELECT_ALL": "Select all ({count})", + "DELETE_CONTACTS": "Delete", + "DELETE_SUCCESS": "Contacts deleted successfully.", + "DELETE_FAILED": "Failed to delete contacts.", + "DELETE_DIALOG": { + "TITLE": "Delete selected contacts", + "SINGULAR_TITLE": "Delete selected contact", + "DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.", + "SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.", + "CONFIRM_MULTIPLE": "Delete contacts", + "CONFIRM_SINGLE": "Delete contact" + } }, "COMPOSE_NEW_CONVERSATION": { diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue index 24f0c7aa5..e8ddd5223 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsBulkActionBar.vue @@ -6,6 +6,7 @@ import { vOnClickOutside } from '@vueuse/components'; import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import LabelActions from 'dashboard/components/widgets/conversation/conversationBulkActions/LabelActions.vue'; +import Policy from 'dashboard/components/policy.vue'; const props = defineProps({ visibleContactIds: { @@ -22,7 +23,12 @@ const props = defineProps({ }, }); -const emit = defineEmits(['clearSelection', 'assignLabels', 'toggleAll']); +const emit = defineEmits([ + 'clearSelection', + 'assignLabels', + 'toggleAll', + 'deleteSelected', +]); const { t } = useI18n(); @@ -139,6 +145,21 @@ const handleAssignLabels = labels => { /> + +