mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 11:37:58 +00:00
feat: Delete all/read notifications (#8844)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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', {});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
15
app/jobs/notification/delete_notification_job.rb
Normal file
15
app/jobs/notification/delete_notification_job.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
38
spec/jobs/notification/delete_notification_job_spec.rb
Normal file
38
spec/jobs/notification/delete_notification_job_spec.rb
Normal 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
|
||||
Reference in New Issue
Block a user