diff --git a/Gemfile.lock b/Gemfile.lock index 80a78c6b6..2f4da34e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -594,7 +594,7 @@ GEM oj (3.16.10) bigdecimal (>= 3.0) ostruct (>= 0.2) - omniauth (2.1.3) + omniauth (2.1.4) hashie (>= 3.4.6) logger rack (>= 2.2.3) @@ -653,7 +653,7 @@ GEM rack (>= 2.0.0) rack-mini-profiler (3.2.0) rack (>= 1.2.0) - rack-protection (4.1.1) + rack-protection (4.2.1) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) diff --git a/app.json b/app.json index 08e725c8e..91fb0fbd5 100644 --- a/app.json +++ b/app.json @@ -36,6 +36,10 @@ "REDIS_OPENSSL_VERIFY_MODE":{ "description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues", "value": "none" + }, + "NODE_OPTIONS": { + "description": "Increase V8 heap for Vite build to avoid OOM", + "value": "--max-old-space-size=4096" } }, "formation": { diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 12a74ed9c..857d901e5 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -23,7 +23,7 @@ class Messages::MessageBuilder process_emails # When the message has no quoted content, it will just be rendered as a regular message # The frontend is equipped to handle this case - process_email_content if @account.feature_enabled?(:quoted_email_reply) + process_email_content @message.save! @message end diff --git a/app/controllers/api/v1/accounts/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/bulk_actions_controller.rb index 34db47861..1b8babbc9 100644 --- a/app/controllers/api/v1/accounts/bulk_actions_controller.rb +++ b/app/controllers/api/v1/accounts/bulk_actions_controller.rb @@ -1,13 +1,11 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController - before_action :type_matches? - def create - if type_matches? - ::BulkActionsJob.perform_later( - account: @current_account, - user: current_user, - params: permitted_params - ) + case normalized_type + when 'Conversation' + enqueue_conversation_job + head :ok + when 'Contact' + enqueue_contact_job head :ok else render json: { success: false }, status: :unprocessable_entity @@ -16,11 +14,34 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll private - def type_matches? - ['Conversation'].include?(params[:type]) + def normalized_type + params[:type].to_s.camelize end - def permitted_params + def enqueue_conversation_job + ::BulkActionsJob.perform_later( + account: @current_account, + user: current_user, + params: conversation_params + ) + end + + def enqueue_contact_job + Contacts::BulkActionJob.perform_later( + @current_account.id, + current_user.id, + contact_params + ) + end + + def conversation_params params.permit(:type, :snoozed_until, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) 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 + end end diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue index 0e893b767..12bed151d 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue @@ -8,6 +8,7 @@ import Button from 'dashboard/components-next/button/Button.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; import Flag from 'dashboard/components-next/flag/Flag.vue'; import ContactDeleteSection from 'dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue'; +import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue'; import countries from 'shared/constants/countries'; const props = defineProps({ @@ -20,9 +21,17 @@ const props = defineProps({ availabilityStatus: { type: String, default: null }, isExpanded: { type: Boolean, default: false }, isUpdating: { type: Boolean, default: false }, + selectable: { type: Boolean, default: false }, + isSelected: { type: Boolean, default: false }, }); -const emit = defineEmits(['toggle', 'updateContact', 'showContact']); +const emit = defineEmits([ + 'toggle', + 'updateContact', + 'showContact', + 'select', + 'avatarHover', +]); const { t } = useI18n(); @@ -88,111 +97,148 @@ const onClickExpand = () => { }; const onClickViewDetails = () => emit('showContact', props.id); + +const toggleSelect = checked => { + emit('select', checked); +}; + +const handleAvatarHover = isHovered => { + emit('avatarHover', isHovered); +}; diff --git a/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue b/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue index 7acc2ff55..72bc2c7dd 100644 --- a/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue +++ b/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue @@ -10,7 +10,15 @@ import { } from 'shared/helpers/CustomErrors'; import ContactsCard from 'dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue'; -defineProps({ contacts: { type: Array, required: true } }); +const props = defineProps({ + contacts: { type: Array, required: true }, + selectedContactIds: { + type: Array, + default: () => [], + }, +}); + +const emit = defineEmits(['toggleContact']); const { t } = useI18n(); const store = useStore(); @@ -20,6 +28,9 @@ const route = useRoute(); const uiFlags = useMapGetter('contacts/getUIFlags'); const isUpdating = computed(() => uiFlags.value.isUpdating); const expandedCardId = ref(null); +const hoveredAvatarId = ref(null); + +const selectedIdsSet = computed(() => new Set(props.selectedContactIds || [])); const updateContact = async updatedData => { try { @@ -58,25 +69,43 @@ const onClickViewDetails = async id => { const toggleExpanded = id => { expandedCardId.value = expandedCardId.value === id ? null : id; }; + +const isSelected = id => selectedIdsSet.value.has(id); + +const shouldShowSelection = id => { + return hoveredAvatarId.value === id || isSelected(id); +}; + +const handleSelect = (id, value) => { + emit('toggleContact', { id, value }); +}; + +const handleAvatarHover = (id, isHovered) => { + hoveredAvatarId.value = isHovered ? id : null; +}; diff --git a/app/javascript/dashboard/components-next/EmptyStateLayout.vue b/app/javascript/dashboard/components-next/EmptyStateLayout.vue index f2744dde5..39eda2d85 100644 --- a/app/javascript/dashboard/components-next/EmptyStateLayout.vue +++ b/app/javascript/dashboard/components-next/EmptyStateLayout.vue @@ -14,6 +14,10 @@ defineProps({ type: Array, default: () => [], }, + showBackdrop: { + type: Boolean, + default: true, + }, }); @@ -25,14 +29,24 @@ defineProps({ class="relative w-full max-w-[60rem] mx-auto overflow-hidden h-full max-h-[28rem]" >
-
+

{{ subtitle }} diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue index 495db1838..c394f1b6b 100644 --- a/app/javascript/dashboard/components-next/captain/PageLayout.vue +++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue @@ -114,6 +114,7 @@ const handlePageChange = event => {

+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue b/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue index 94cab9a33..b7ee59ead 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue @@ -61,23 +61,26 @@ const bulkCheckboxState = computed({ >
-
+
- + {{ selectAllLabel }}
- + {{ selectedCountLabel }} +
+
-