mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +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
|
head :ok
|
||||||
end
|
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
|
def unread_count
|
||||||
@unread_count = notification_finder.unread_count
|
@unread_count = notification_finder.unread_count
|
||||||
render json: @unread_count
|
render json: @unread_count
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ class NotificationsAPI extends ApiClient {
|
|||||||
delete(id) {
|
delete(id) {
|
||||||
return axios.delete(`${this.url}/${id}`);
|
return axios.delete(`${this.url}/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAll({ type = 'all' }) {
|
||||||
|
return axios.post(`${this.url}/destroy_all`, {
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NotificationsAPI();
|
export default new NotificationsAPI();
|
||||||
|
|||||||
@@ -115,18 +115,6 @@ export default {
|
|||||||
value: 'read',
|
value: 'read',
|
||||||
selected: true,
|
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: [
|
sortOptions: [
|
||||||
{
|
{
|
||||||
@@ -137,10 +125,6 @@ export default {
|
|||||||
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.OLDEST'),
|
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.OLDEST'),
|
||||||
key: 'oldest',
|
key: 'oldest',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.PRIORITY'),
|
|
||||||
key: 'priority',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
activeSort: 'newest',
|
activeSort: 'newest',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
v-if="showInboxOptionMenu"
|
v-if="showInboxOptionMenu"
|
||||||
v-on-clickaway="openInboxOptionsMenu"
|
v-on-clickaway="openInboxOptionsMenu"
|
||||||
class="absolute top-9"
|
class="absolute top-9"
|
||||||
|
@option-click="onInboxOptionMenuClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import InboxOptionMenu from './InboxOptionMenu.vue';
|
import InboxOptionMenu from './InboxOptionMenu.vue';
|
||||||
|
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
import InboxDisplayMenu from './InboxDisplayMenu.vue';
|
import InboxDisplayMenu from './InboxDisplayMenu.vue';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -71,12 +73,34 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
openInboxDisplayMenu() {
|
||||||
this.showInboxDisplayMenu = !this.showInboxDisplayMenu;
|
this.showInboxDisplayMenu = !this.showInboxDisplayMenu;
|
||||||
},
|
},
|
||||||
openInboxOptionsMenu() {
|
openInboxOptionsMenu() {
|
||||||
this.showInboxOptionMenu = !this.showInboxOptionMenu;
|
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>
|
</script>
|
||||||
|
|||||||
@@ -7,15 +7,7 @@
|
|||||||
v-for="item in menuItems"
|
v-for="item in menuItems"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
@click="onMenuItemClick(item.key)"
|
@click="onClick(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)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,24 +22,6 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
menuItems: [
|
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',
|
key: 'mark_all_read',
|
||||||
label: this.$t('INBOX.MENU_ITEM.MARK_ALL_READ'),
|
label: this.$t('INBOX.MENU_ITEM.MARK_ALL_READ'),
|
||||||
@@ -64,7 +38,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onMenuItemClick(key) {
|
onClick(key) {
|
||||||
this.$emit('option-click', 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) {
|
addNotification({ commit }, data) {
|
||||||
commit(types.ADD_NOTIFICATION, data);
|
commit(types.ADD_NOTIFICATION, data);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,4 +61,15 @@ export const mutations = {
|
|||||||
[types.SET_ALL_NOTIFICATIONS_LOADED]: $state => {
|
[types.SET_ALL_NOTIFICATIONS_LOADED]: $state => {
|
||||||
Vue.set($state.uiFlags, 'isAllNotificationsLoaded', true);
|
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]]);
|
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 });
|
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',
|
EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS',
|
||||||
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
||||||
SET_ALL_NOTIFICATIONS_LOADED: 'SET_ALL_NOTIFICATIONS_LOADED',
|
SET_ALL_NOTIFICATIONS_LOADED: 'SET_ALL_NOTIFICATIONS_LOADED',
|
||||||
|
DELETE_READ_NOTIFICATIONS: 'DELETE_READ_NOTIFICATIONS',
|
||||||
|
DELETE_ALL_NOTIFICATIONS: 'DELETE_ALL_NOTIFICATIONS',
|
||||||
|
|
||||||
// Contact Conversation
|
// Contact Conversation
|
||||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
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
|
collection do
|
||||||
post :read_all
|
post :read_all
|
||||||
get :unread_count
|
get :unread_count
|
||||||
|
post :destroy_all
|
||||||
end
|
end
|
||||||
member do
|
member do
|
||||||
post :snooze
|
post :snooze
|
||||||
|
|||||||
@@ -207,4 +207,43 @@ RSpec.describe 'Notifications API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
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
|
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