From 39e27d2a23b2dd2e455d3c04ac6c2dc6fb9a6d73 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 5 Feb 2024 13:33:05 +0530 Subject: [PATCH] feat: Delete all/read notifications (#8844) --- .../v1/accounts/notifications_controller.rb | 9 +++++ app/javascript/dashboard/api/notifications.js | 6 +++ .../inbox/components/InboxDisplayMenu.vue | 16 -------- .../inbox/components/InboxListHeader.vue | 24 +++++++++++ .../inbox/components/InboxOptionMenu.vue | 30 +------------- .../store/modules/notifications/actions.js | 24 +++++++++++ .../store/modules/notifications/mutations.js | 11 +++++ .../specs/notifications/actions.spec.js | 40 +++++++++++++++++++ .../specs/notifications/mutations.spec.js | 28 +++++++++++++ .../dashboard/store/mutation-types.js | 2 + .../notification/delete_notification_job.rb | 15 +++++++ config/routes.rb | 1 + .../accounts/notifications_controller_spec.rb | 39 ++++++++++++++++++ .../delete_notification_job_spec.rb | 38 ++++++++++++++++++ 14 files changed, 239 insertions(+), 44 deletions(-) create mode 100644 app/jobs/notification/delete_notification_job.rb create mode 100644 spec/jobs/notification/delete_notification_job_spec.rb diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index 54a03c783..0eeff5695 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -39,6 +39,15 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro head :ok end + def destroy_all + if params[:type] == 'read' + ::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read) + else + ::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all) + end + head :ok + end + def unread_count @unread_count = notification_finder.unread_count render json: @unread_count diff --git a/app/javascript/dashboard/api/notifications.js b/app/javascript/dashboard/api/notifications.js index aa6413483..e6a4edf3e 100644 --- a/app/javascript/dashboard/api/notifications.js +++ b/app/javascript/dashboard/api/notifications.js @@ -36,6 +36,12 @@ class NotificationsAPI extends ApiClient { delete(id) { return axios.delete(`${this.url}/${id}`); } + + deleteAll({ type = 'all' }) { + return axios.post(`${this.url}/destroy_all`, { + type, + }); + } } export default new NotificationsAPI(); diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxDisplayMenu.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxDisplayMenu.vue index 24316270c..ba4c313ba 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxDisplayMenu.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxDisplayMenu.vue @@ -115,18 +115,6 @@ export default { value: 'read', selected: true, }, - { - id: 3, - name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.LABELS'), - value: 'labels', - selected: false, - }, - { - id: 4, - name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.CONVERSATION_ID'), - value: 'conversationId', - selected: false, - }, ], sortOptions: [ { @@ -137,10 +125,6 @@ export default { name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.OLDEST'), key: 'oldest', }, - { - name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.PRIORITY'), - key: 'priority', - }, ], activeSort: 'newest', }; diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue index d58e89230..6e7f3b894 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxListHeader.vue @@ -49,6 +49,7 @@ v-if="showInboxOptionMenu" v-on-clickaway="openInboxOptionsMenu" class="absolute top-9" + @option-click="onInboxOptionMenuClick" /> @@ -57,6 +58,7 @@ diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxOptionMenu.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxOptionMenu.vue index 308225a49..1ad9385bf 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxOptionMenu.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxOptionMenu.vue @@ -7,15 +7,7 @@ v-for="item in menuItems" :key="item.key" :label="item.label" - @click="onMenuItemClick(item.key)" - /> - -
-
@@ -30,24 +22,6 @@ export default { data() { return { menuItems: [ - { - key: 'mark_as_read', - label: this.$t('INBOX.MENU_ITEM.MARK_AS_READ'), - }, - { - key: 'mark_as_unread', - label: this.$t('INBOX.MENU_ITEM.MARK_AS_UNREAD'), - }, - { - key: 'snooze', - label: this.$t('INBOX.MENU_ITEM.SNOOZE'), - }, - { - key: 'delete', - label: this.$t('INBOX.MENU_ITEM.DELETE'), - }, - ], - commonMenuItems: [ { key: 'mark_all_read', label: this.$t('INBOX.MENU_ITEM.MARK_ALL_READ'), @@ -64,7 +38,7 @@ export default { }; }, methods: { - onMenuItemClick(key) { + onClick(key) { this.$emit('option-click', key); }, }, diff --git a/app/javascript/dashboard/store/modules/notifications/actions.js b/app/javascript/dashboard/store/modules/notifications/actions.js index 559b560d4..47d2e1f1f 100644 --- a/app/javascript/dashboard/store/modules/notifications/actions.js +++ b/app/javascript/dashboard/store/modules/notifications/actions.js @@ -95,6 +95,30 @@ export const actions = { } }, + deleteAllRead: async ({ commit }) => { + commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }); + try { + await NotificationsAPI.deleteAll({ + type: 'read', + }); + commit(types.DELETE_READ_NOTIFICATIONS); + commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }); + } catch (error) { + commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }); + } + }, + deleteAll: async ({ commit }) => { + commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }); + try { + await NotificationsAPI.deleteAll({ + type: 'all', + }); + commit(types.DELETE_ALL_NOTIFICATIONS); + commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }); + } catch (error) { + commit(types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }); + } + }, addNotification({ commit }, data) { commit(types.ADD_NOTIFICATION, data); }, diff --git a/app/javascript/dashboard/store/modules/notifications/mutations.js b/app/javascript/dashboard/store/modules/notifications/mutations.js index 14f2bbb0f..55036b1d5 100644 --- a/app/javascript/dashboard/store/modules/notifications/mutations.js +++ b/app/javascript/dashboard/store/modules/notifications/mutations.js @@ -61,4 +61,15 @@ export const mutations = { [types.SET_ALL_NOTIFICATIONS_LOADED]: $state => { Vue.set($state.uiFlags, 'isAllNotificationsLoaded', true); }, + + [types.DELETE_READ_NOTIFICATIONS]: $state => { + Object.values($state.records).forEach(item => { + if (item.read_at) { + Vue.delete($state.records, item.id); + } + }); + }, + [types.DELETE_ALL_NOTIFICATIONS]: $state => { + Vue.set($state, 'records', {}); + }, }; diff --git a/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js b/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js index be3af6f06..8e2712a3d 100644 --- a/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js @@ -219,4 +219,44 @@ describe('#actions', () => { expect(commit.mock.calls).toEqual([[types.CLEAR_NOTIFICATIONS]]); }); }); + + describe('deleteAllRead', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({}); + await actions.deleteAllRead({ commit }); + expect(commit.mock.calls).toEqual([ + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }], + [types.DELETE_READ_NOTIFICATIONS], + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await actions.deleteAllRead({ commit }); + expect(commit.mock.calls).toEqual([ + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }], + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }], + ]); + }); + }); + + describe('deleteAll', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({}); + await actions.deleteAll({ commit }); + expect(commit.mock.calls).toEqual([ + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }], + [types.DELETE_ALL_NOTIFICATIONS], + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await actions.deleteAll({ commit }); + expect(commit.mock.calls).toEqual([ + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: true }], + [types.SET_NOTIFICATIONS_UI_FLAG, { isDeleting: false }], + ]); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js index 1efb06e59..35fa1050f 100644 --- a/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js @@ -155,4 +155,32 @@ describe('#mutations', () => { expect(state.uiFlags).toEqual({ isAllNotificationsLoaded: true }); }); }); + + describe('#DELETE_READ_NOTIFICATIONS', () => { + it('delete read notifications', () => { + const state = { + records: { + 1: { id: 1, primary_actor_id: 1, read_at: true }, + 2: { id: 2, primary_actor_id: 2 }, + }, + }; + mutations[types.DELETE_READ_NOTIFICATIONS](state); + expect(state.records).toEqual({ + 2: { id: 2, primary_actor_id: 2 }, + }); + }); + }); + + describe('#DELETE_ALL_NOTIFICATIONS', () => { + it('delete all notifications', () => { + const state = { + records: { + 1: { id: 1, primary_actor_id: 1, read_at: true }, + 2: { id: 2, primary_actor_id: 2 }, + }, + }; + mutations[types.DELETE_ALL_NOTIFICATIONS](state); + expect(state.records).toEqual({}); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 4f702105f..aae2b3bc4 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -141,6 +141,8 @@ export default { EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS', UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE', SET_ALL_NOTIFICATIONS_LOADED: 'SET_ALL_NOTIFICATIONS_LOADED', + DELETE_READ_NOTIFICATIONS: 'DELETE_READ_NOTIFICATIONS', + DELETE_ALL_NOTIFICATIONS: 'DELETE_ALL_NOTIFICATIONS', // Contact Conversation SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG', diff --git a/app/jobs/notification/delete_notification_job.rb b/app/jobs/notification/delete_notification_job.rb new file mode 100644 index 000000000..9efaf472c --- /dev/null +++ b/app/jobs/notification/delete_notification_job.rb @@ -0,0 +1,15 @@ +class Notification::DeleteNotificationJob < ApplicationJob + queue_as :low + + def perform(user, type: :all) + ActiveRecord::Base.transaction do + if type == :all + # Delete all notifications + user.notifications.destroy_all + elsif type == :read + # Delete only read notifications + user.notifications.where.not(read_at: nil).destroy_all + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index e0c636e0f..653ba8277 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -173,6 +173,7 @@ Rails.application.routes.draw do collection do post :read_all get :unread_count + post :destroy_all end member do post :snooze diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb index 55ebf997c..3dcb47f71 100644 --- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb @@ -207,4 +207,43 @@ RSpec.describe 'Notifications API', type: :request do end end end + + describe 'POST /api/v1/accounts/{account.id}/notifications/destroy_all' do + let(:admin) { create(:user, account: account, role: :administrator) } + let(:notification1) { create(:notification, account: account, user: admin) } + let(:notification2) { create(:notification, account: account, user: admin) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/notifications/destroy_all" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:admin) { create(:user, account: account, role: :administrator) } + + it 'deletes all the read notifications' do + expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, type: :read) + + post "/api/v1/accounts/#{account.id}/notifications/destroy_all", + headers: admin.create_new_auth_token, + params: { type: 'read' }, + as: :json + + expect(response).to have_http_status(:success) + end + + it 'deletes all the notifications' do + expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, type: :all) + + post "/api/v1/accounts/#{account.id}/notifications/destroy_all", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + end + end + end end diff --git a/spec/jobs/notification/delete_notification_job_spec.rb b/spec/jobs/notification/delete_notification_job_spec.rb new file mode 100644 index 000000000..194a24732 --- /dev/null +++ b/spec/jobs/notification/delete_notification_job_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe Notification::DeleteNotificationJob do + let(:user) { create(:user) } + let(:conversation) { create(:conversation) } + + context 'when enqueuing the job' do + it 'enqueues the job to delete all notifications' do + expect do + described_class.perform_later(user.id, type: :all) + end.to have_enqueued_job(described_class).on_queue('low') + end + + it 'enqueues the job to delete read notifications' do + expect do + described_class.perform_later(user.id, type: :read) + end.to have_enqueued_job(described_class).on_queue('low') + end + end + + context 'when performing the job' do + before do + create(:notification, user: user, read_at: nil) + create(:notification, user: user, read_at: Time.current) + end + + it 'deletes all notifications' do + described_class.perform_now(user, type: :all) + expect(user.notifications.count).to eq(0) + end + + it 'deletes only read notifications' do + described_class.perform_now(user, type: :read) + expect(user.notifications.count).to eq(1) + expect(user.notifications.where(read_at: nil).count).to eq(1) + end + end +end