feat: Delete all/read notifications (#8844)

This commit is contained in:
Muhsin Keloth
2024-02-05 13:33:05 +05:30
committed by GitHub
parent 45e630fc60
commit 39e27d2a23
14 changed files with 239 additions and 44 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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',
};

View File

@@ -49,6 +49,7 @@
v-if="showInboxOptionMenu"
v-on-clickaway="openInboxOptionsMenu"
class="absolute top-9"
@option-click="onInboxOptionMenuClick"
/>
</div>
</div>
@@ -57,6 +58,7 @@
<script>
import { mixin as clickaway } from 'vue-clickaway';
import InboxOptionMenu from './InboxOptionMenu.vue';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import InboxDisplayMenu from './InboxDisplayMenu.vue';
export default {
components: {
@@ -71,12 +73,34 @@ export default {
};
},
methods: {
markAllRead() {
this.$track(INBOX_EVENTS.MARK_ALL_NOTIFICATIONS_AS_READ);
this.$store.dispatch('notifications/readAll');
},
deleteAll() {
this.$store.dispatch('notifications/deleteAll');
},
deleteAllRead() {
this.$store.dispatch('notifications/deleteAllRead');
},
openInboxDisplayMenu() {
this.showInboxDisplayMenu = !this.showInboxDisplayMenu;
},
openInboxOptionsMenu() {
this.showInboxOptionMenu = !this.showInboxOptionMenu;
},
onInboxOptionMenuClick(key) {
this.showInboxOptionMenu = false;
if (key === 'mark_all_read') {
this.markAllRead();
}
if (key === 'delete_all') {
this.deleteAll();
}
if (key === 'delete_all_read') {
this.deleteAllRead();
}
},
},
};
</script>

View File

@@ -7,15 +7,7 @@
v-for="item in menuItems"
:key="item.key"
:label="item.label"
@click="onMenuItemClick(item.key)"
/>
</div>
<div class="flex flex-col">
<menu-item
v-for="item in commonMenuItems"
:key="item.key"
:label="item.label"
@click="onMenuItemClick(item.key)"
@click="onClick(item.key)"
/>
</div>
</div>
@@ -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);
},
},

View File

@@ -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);
},

View File

@@ -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', {});
},
};

View File

@@ -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 }],
]);
});
});
});

View File

@@ -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({});
});
});
});

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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