mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 18:22:53 +00:00
fix: Inbox view Read/Snoozed display filters (#8907)
* fix: Notification filters * Update notification_finder.rb * Update notification_finder.rb * Update notification_finder.rb * fix: spec * fix: specs * Update notification_finder.rb * fix: add more fixes * Update notification_finder.rb * fix: specs * chore: better comments * chore: removed filtering * chore: refactoring * fix: review fixes * fix: API call * chore: Minor fix * Rename spec * Fix params getting undefined * Fix finder --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -7,8 +7,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@notifications = notification_finder.notifications
|
||||
@unread_count = notification_finder.unread_count
|
||||
@notifications = notification_finder.perform
|
||||
@count = notification_finder.count
|
||||
end
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ class NotificationFinder
|
||||
set_up
|
||||
end
|
||||
|
||||
def perform
|
||||
notifications
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order)
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@@ -26,27 +26,31 @@ class NotificationFinder
|
||||
|
||||
def set_up
|
||||
find_all_notifications
|
||||
filter_by_read_status
|
||||
filter_by_status
|
||||
filter_snoozed_notifications
|
||||
fitler_read_notifications
|
||||
end
|
||||
|
||||
def find_all_notifications
|
||||
@notifications = current_user.notifications.where(account_id: @current_account.id)
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
|
||||
def filter_snoozed_notifications
|
||||
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
|
||||
end
|
||||
|
||||
def filter_by_read_status
|
||||
@notifications = @notifications.where.not(read_at: nil) if params[:type] == 'read'
|
||||
def fitler_read_notifications
|
||||
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
|
||||
end
|
||||
|
||||
def type_included?(type)
|
||||
(params[:includes] || []).include?(type)
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: params[:sort_order] || :desc)
|
||||
def sort_order
|
||||
params[:sort_order] || :desc
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,12 +7,13 @@ class NotificationsAPI extends ApiClient {
|
||||
}
|
||||
|
||||
get({ page, status, type, sortOrder }) {
|
||||
const includesFilter = [status, type].filter(value => !!value);
|
||||
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
status,
|
||||
type,
|
||||
sort_order: sortOrder,
|
||||
includes: includesFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,20 +27,36 @@ describe('#NotificationAPI', () => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'Conversation',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
describe('#get', () => {
|
||||
it('generates the API call if both params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'Conversation',
|
||||
sort_order: 'desc',
|
||||
},
|
||||
status: 'snoozed',
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['snoozed', 'read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates the API call if one of the params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { applyInboxPageFilters, sortComparator } from './helpers';
|
||||
import { sortComparator } from './helpers';
|
||||
|
||||
export const getters = {
|
||||
getNotifications($state) {
|
||||
@@ -6,11 +6,8 @@ export const getters = {
|
||||
},
|
||||
getFilteredNotifications: $state => filters => {
|
||||
const sortOrder = filters.sortOrder === 'desc' ? 'newest' : 'oldest';
|
||||
const filteredNotifications = Object.values($state.records).filter(
|
||||
notification => applyInboxPageFilters(notification, filters)
|
||||
);
|
||||
const sortedNotifications = filteredNotifications.sort((a, b) =>
|
||||
sortComparator(a, b, sortOrder)
|
||||
const sortedNotifications = Object.values($state.records).sort((n1, n2) =>
|
||||
sortComparator(n1, n2, sortOrder)
|
||||
);
|
||||
return sortedNotifications;
|
||||
},
|
||||
|
||||
@@ -1,31 +1,3 @@
|
||||
export const filterByStatus = (snoozedUntil, filterStatus) =>
|
||||
filterStatus === 'snoozed' ? !!snoozedUntil : !snoozedUntil;
|
||||
|
||||
export const filterByType = (readAt, filterType) =>
|
||||
filterType === 'read' ? !!readAt : !readAt;
|
||||
|
||||
export const filterByTypeAndStatus = (
|
||||
readAt,
|
||||
snoozedUntil,
|
||||
filterType,
|
||||
filterStatus
|
||||
) => {
|
||||
const shouldFilterByStatus = filterByStatus(snoozedUntil, filterStatus);
|
||||
const shouldFilterByType = filterByType(readAt, filterType);
|
||||
return shouldFilterByStatus && shouldFilterByType;
|
||||
};
|
||||
|
||||
export const applyInboxPageFilters = (notification, filters) => {
|
||||
const { status, type } = filters;
|
||||
const { read_at: readAt, snoozed_until: snoozedUntil } = notification;
|
||||
|
||||
if (status && type)
|
||||
return filterByTypeAndStatus(readAt, snoozedUntil, type, status);
|
||||
if (status && !type) return filterByStatus(snoozedUntil, status);
|
||||
if (!status && type) return filterByType(readAt, type);
|
||||
return true;
|
||||
};
|
||||
|
||||
const INBOX_SORT_OPTIONS = {
|
||||
newest: 'desc',
|
||||
oldest: 'asc',
|
||||
|
||||
@@ -34,6 +34,8 @@ describe('#getters', () => {
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
expect(getters.getFilteredNotifications(state)(filters)).toEqual([
|
||||
{ id: 1, read_at: '2024-02-07T11:42:39.988Z', snoozed_until: null },
|
||||
{ id: 2, read_at: null, snoozed_until: null },
|
||||
{
|
||||
id: 3,
|
||||
read_at: '2024-02-07T11:42:39.988Z',
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
filterByStatus,
|
||||
filterByType,
|
||||
filterByTypeAndStatus,
|
||||
applyInboxPageFilters,
|
||||
sortComparator,
|
||||
} from '../../notifications/helpers';
|
||||
import { sortComparator } from '../../notifications/helpers';
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
@@ -45,126 +39,6 @@ const notifications = [
|
||||
},
|
||||
];
|
||||
|
||||
describe('#filterByStatus', () => {
|
||||
it('returns the notifications with snoozed status', () => {
|
||||
const filters = { status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(
|
||||
filterByStatus(notification.snoozed_until, filters.status)
|
||||
).toEqual(notification.snoozed_until !== null);
|
||||
});
|
||||
});
|
||||
it('returns true if the notification is snoozed', () => {
|
||||
const filters = { status: 'snoozed' };
|
||||
expect(
|
||||
filterByStatus(notifications[3].snoozed_until, filters.status)
|
||||
).toEqual(true);
|
||||
});
|
||||
it('returns false if the notification is not snoozed', () => {
|
||||
const filters = { status: 'snoozed' };
|
||||
expect(
|
||||
filterByStatus(notifications[2].snoozed_until, filters.status)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#filterByType', () => {
|
||||
it('returns the notifications with read status', () => {
|
||||
const filters = { type: 'read' };
|
||||
notifications.forEach(notification => {
|
||||
expect(filterByType(notification.read_at, filters.type)).toEqual(
|
||||
notification.read_at !== null
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns true if the notification is read', () => {
|
||||
const filters = { type: 'read' };
|
||||
expect(filterByType(notifications[0].read_at, filters.type)).toEqual(true);
|
||||
});
|
||||
it('returns false if the notification is not read', () => {
|
||||
const filters = { type: 'read' };
|
||||
expect(filterByType(notifications[1].read_at, filters.type)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#filterByTypeAndStatus', () => {
|
||||
it('returns the notifications with type and status', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(
|
||||
filterByTypeAndStatus(
|
||||
notification.read_at,
|
||||
notification.snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
).toEqual(
|
||||
notification.read_at !== null && notification.snoozed_until !== null
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns true if the notification is read and snoozed', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
expect(
|
||||
filterByTypeAndStatus(
|
||||
notifications[4].read_at,
|
||||
notifications[4].snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
).toEqual(true);
|
||||
});
|
||||
it('returns false if the notification is not read and snoozed', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
expect(
|
||||
filterByTypeAndStatus(
|
||||
notifications[3].read_at,
|
||||
notifications[3].snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#applyInboxPageFilters', () => {
|
||||
it('returns the notifications with type and status', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(
|
||||
filterByTypeAndStatus(
|
||||
notification.read_at,
|
||||
notification.snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns the notifications with type only', () => {
|
||||
const filters = { type: 'read', status: null };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(
|
||||
filterByType(notification.read_at, filters.type)
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns the notifications with status only', () => {
|
||||
const filters = { type: null, status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(
|
||||
filterByStatus(notification.snoozed_until, filters.status)
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns true if there are no filters', () => {
|
||||
const filters = { type: null, status: null };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sortComparator', () => {
|
||||
it('returns the notifications sorted by newest', () => {
|
||||
const sortOrder = 'newest';
|
||||
|
||||
@@ -6,5 +6,15 @@ FactoryBot.define do
|
||||
notification_type { 'conversation_assignment' }
|
||||
user
|
||||
account
|
||||
read_at { nil }
|
||||
snoozed_until { nil }
|
||||
end
|
||||
|
||||
trait :read do
|
||||
read_at { DateTime.now.utc - 3.days }
|
||||
end
|
||||
|
||||
trait :snoozed do
|
||||
snoozed_until { DateTime.now.utc + 3.days }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,103 +1,79 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe NotificationFinder do
|
||||
subject(:notification_finder) { described_class.new(user, account, params) }
|
||||
|
||||
RSpec.describe NotificationFinder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let(:notification_finder) { described_class.new(user, account, params) }
|
||||
|
||||
before do
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 1.day, last_activity_at: DateTime.now.utc + 1.day,
|
||||
read_at: DateTime.now.utc)
|
||||
create(:notification, account: account, user: user, snoozed_until: DateTime.now.utc + 3.days, updated_at: DateTime.now.utc,
|
||||
last_activity_at: DateTime.now.utc)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 2.days, last_activity_at: DateTime.now.utc + 2.days)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 4.days, last_activity_at: DateTime.now.utc + 4.days,
|
||||
notification_type: :conversation_creation, read_at: DateTime.now.utc)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 5.days, last_activity_at: DateTime.now.utc + 5.days,
|
||||
notification_type: :conversation_mention)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 6.days, last_activity_at: DateTime.now.utc + 6.days,
|
||||
notification_type: :participating_conversation_new_message)
|
||||
create(:notification, :snoozed, account: account, user: user)
|
||||
create_list(:notification, 2, :read, account: account, user: user)
|
||||
create_list(:notification, 3, account: account, user: user)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when params are empty' do
|
||||
describe '#notifications' do
|
||||
subject { notification_finder.notifications }
|
||||
|
||||
context 'with default params (empty)' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'returns all the notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 6
|
||||
end
|
||||
|
||||
it 'orders notifications by last activity at' do
|
||||
result = notification_finder.perform
|
||||
expect(result.first.last_activity_at).to be > result.last.last_activity_at
|
||||
end
|
||||
|
||||
it 'returns unread count' do
|
||||
result = notification_finder.unread_count
|
||||
expect(result).to be 4
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 6
|
||||
it 'returns all unread and unsnoozed notifications, ordered by last activity' do
|
||||
expect(subject.size).to eq(3)
|
||||
expect(subject).to match_array(subject.sort_by(&:last_activity_at).reverse)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when snoozed param is passed' do
|
||||
let(:params) { { status: 'snoozed' } }
|
||||
context 'with params including read and snoozed statuses' do
|
||||
let(:params) { { includes: %w[read snoozed] } }
|
||||
|
||||
it 'returns only snoozed notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 1
|
||||
end
|
||||
|
||||
it 'returns unread count' do
|
||||
result = notification_finder.unread_count
|
||||
expect(result).to be 1
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 1
|
||||
it 'returns all notifications, including read and snoozed' do
|
||||
expect(subject.size).to eq(6)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when type read param is passed' do
|
||||
let(:params) { { type: 'read' } }
|
||||
context 'with params including only read status' do
|
||||
let(:params) { { includes: ['read'] } }
|
||||
|
||||
it 'returns only read notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 2
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 2
|
||||
it 'returns all notifications expect the snoozed' do
|
||||
expect(subject.size).to eq(5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when type read and snoozed param is passed' do
|
||||
let(:params) { { type: 'read', status: 'snoozed' } }
|
||||
context 'with params including only snoozed status' do
|
||||
let(:params) { { includes: ['snoozed'] } }
|
||||
|
||||
it 'returns only read notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 0
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 0
|
||||
it 'rreturns all notifications only expect the read' do
|
||||
expect(subject.size).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sort order is passed' do
|
||||
context 'with ascending sort order' do
|
||||
let(:params) { { sort_order: :asc } }
|
||||
|
||||
it 'returns notifications in ascending order' do
|
||||
result = notification_finder.perform
|
||||
expect(result.first.last_activity_at).to be < result.last.last_activity_at
|
||||
it 'returns notifications in ascending order by last activity' do
|
||||
expect(subject.first.last_activity_at).to be < subject.last.last_activity_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'counts' do
|
||||
subject { notification_finder }
|
||||
|
||||
context 'without specific filters' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'correctly reports unread and total counts' do
|
||||
expect(subject.unread_count).to eq(3)
|
||||
expect(subject.count).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with filters applied' do
|
||||
let(:params) { { includes: %w[read snoozed] } }
|
||||
|
||||
it 'adjusts counts based on included statuses' do
|
||||
expect(subject.unread_count).to eq(4)
|
||||
expect(subject.count).to eq(6)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user