Project

General

Profile

« Previous | Next » 

Revision ea0d94e6

Added by Ohad Levy about 7 years ago

fixes #19192 - adds mark all as read notification action

This implements missing feature from patternfly ( see http://www.patternfly.org/pattern-library/communication/notification-drawer/#/code/angular)

View differences:

app/controllers/notification_recipients_controller.rb
process_response @notification_recipient.destroy
end
def update_group_as_read
count = NotificationRecipient.
joins(:notification_blueprint).
where(user_id: User.current.id, seen: false,
notification_blueprints: { group: params[:group]}).
update_all(seen: true)
logger.debug("updated #{count} notification recipents as seen for group #{params[:group]}")
UINotifications::CacheHandler.new(User.current.id).clear unless count.zero?
head status: (count.zero? ? :not_modified : :ok)
end
private
def require_login
app/services/foreman/access_permissions.rb
permission_set.security_block :public do |map|
map.permission :user_logout, { :users => [:logout] }, :public => true
map.permission :my_account, { :users => [:edit],
:notification_recipients => [:index, :update, :destroy] }, :public => true
:notification_recipients => [:index, :update, :destroy, :update_group_as_read] }, :public => true
map.permission :api_status, { :"api/v1/home" => [:status], :"api/v2/home" => [:status]}, :public => true
map.permission :about_index, { :about => [:index] }, :public => true
end
config/routes.rb
end
end
resources :notification_recipients, :only => [:index, :update, :destroy]
resources :notification_recipients, :only => [:index, :update, :destroy] do
collection do
put 'group/:group' => 'notification_recipients#update_group_as_read'
end
end
end
package.json
],
"moduleNameMapper": {
"^.+\\.(png|gif|css|scss)$": "identity-obj-proxy"
},
"globals": {
"__testing__": true
}
}
}
test/controllers/notification_recipients_controller_test.rb
assert_equal "#{host} has no owner set", response['notifications'][0]["text"]
end
test 'group mark as read' do
add_notification
query = {user_id: User.current.id, seen: false}
assert_equal 1, NotificationRecipient.where(query).count
put :update_group_as_read, { :group => 'Testing' }, set_session_user
assert_response :success
assert_equal 1, NotificationRecipient.where(query.merge({seen: true})).count
assert_equal 0, NotificationRecipient.where(query).count
end
test 'group mark as read twice' do
add_notification
put :update_group_as_read, { :group => 'Testing' }, set_session_user
assert_response :success
put :update_group_as_read, { :group => 'Testing' }, set_session_user
assert_response :not_modified
end
test 'invalid group mark as read' do
put :update_group_as_read, { :group => 'unknown;INSERT INTO users (user_id, group)' }, set_session_user
assert_response :not_modified
end
test 'group mark as read only update the correct group' do
add_notification('Group1')
add_notification('Group2')
query = {user_id: User.current.id, seen: false}
assert_equal 2, NotificationRecipient.where(query).count
put :update_group_as_read, { :group => 'Group1' }, set_session_user
assert_response :success
assert_equal 1, NotificationRecipient.where(query.merge({seen: true})).count
assert_equal 0, NotificationRecipient.where(query).
joins(:notification_blueprint).
where(notification_blueprints: { group: 'Group1' }).count
assert_equal 1, NotificationRecipient.where(query).
joins(:notification_blueprint).
where(notification_blueprints: { group: 'Group2' }).count
end
private
def add_notification
def add_notification(group = 'Testing')
type = FactoryGirl.create(:notification_blueprint,
:group => group,
:message => 'this test just executed successfully')
FactoryGirl.create(:notification, :notification_blueprint => type, :audience => 'global')
end
webpack/assets/javascripts/react_app/API.js
dataType: 'json',
data: data,
error: function (jqXHR, textStatus, errorThrown) {
/* eslint-disable no-console */
console.log(jqXHR);
}
});
},
markGroupNotificationAsRead(group) {
$.ajax({
url: `/notification_recipients/group/${group}`,
contentType: 'application/json',
type: 'PUT',
error: function (jqXHR, textStatus, errorThrown) {
/* eslint-disable no-console */
console.log(jqXHR);
}
});
webpack/assets/javascripts/react_app/components/notifications/drawer/index.js
expandedGroup,
onExpandGroup,
onMarkAsRead,
onMarkGroupAsRead,
onClickedLink
}
) => {
......
key={key}
onClickedLink={onClickedLink}
onMarkAsRead={onMarkAsRead}
onMarkGroupAsRead={onMarkGroupAsRead}
isExpanded={expandedGroup === key}
onExpand={onExpandGroup}
notifications={notificationGroups[key]}
webpack/assets/javascripts/react_app/components/notifications/drawer/notificationGroup.js
isExpanded,
onExpand,
onMarkAsRead,
onMarkGroupAsRead,
onClickedLink
}
) => {
......
onMarkAsRead={onMarkAsRead.bind(this, group)}
/>
))}
<div className="drawer-pf-action">
<a
className="btn btn-link btn-block"
onClick={onMarkGroupAsRead.bind(this, group)}
disabled={unreadCount === 0}>
{__('Mark All Read')}
</a>
</div>
</div>}
</div>
);
webpack/assets/javascripts/react_app/components/notifications/index.js
expandGroup,
expandedGroup,
onMarkAsRead,
onMarkGroupAsRead,
hasUnreadMessages,
isReady,
onClickedLink
......
onExpandGroup={expandGroup}
onClickedLink={onClickedLink}
onMarkAsRead={onMarkAsRead}
onMarkGroupAsRead={onMarkGroupAsRead}
expandedGroup={expandedGroup}
notificationGroups={notifications}
/>}
webpack/assets/javascripts/react_app/components/notifications/notifications.test.js
import React from 'react';
import { shallow, mount } from 'enzyme';
import {shallow, mount} from 'enzyme';
import Notifications from './';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import Store from '../../redux';
import {getStore} from '../../redux';
import {
emptyState,
emptyHtml,
......
};
$.getJSON = jest.genMockFunction().mockImplementation(url => {
return new Promise(resolve => resolve(JSON.parse(serverResponse)));
return {
then: function (callback) {
callback(JSON.parse(serverResponse));
}
};
});
});
......
const store = mockStore(stateWithNotifications);
const box = shallow(<Notifications store={store} />);
expect(
box.render().find('.drawer-pf-notification').length
).toEqual(1);
expect(box.render().find('.drawer-pf-notification').length).toEqual(1);
});
it('should display full bell on a state with unread notifications', () => {
......
expect(box.render().find('.fa-bell').length).toBe(1);
});
it('full flow', done => {
const data = { url: '/notification_recipients' };
const wrapper = mount(<Notifications data={data} store={Store} />);
try {
expect(wrapper.render().find('.fa-bell-o').length).toBe(1);
setTimeout(() => {
const rendered = wrapper.render();
// full bell is rendered
expect(rendered.find('.fa-bell').length).toBe(1);
wrapper.find('.fa-bell').simulate('click');
expect(rendered.find('.panel-group').length).toEqual(0);
setTimeout(() => {
// a panel group is rendered (inside the accordion)
expect(wrapper.find('.panel-group').length).toEqual(1);
wrapper.find('.panel-group .panel-heading').simulate('click');
setTimeout(() => {
expect(wrapper.find('.not-seen').length).toEqual(1);
wrapper.find('.not-seen').simulate('click');
setTimeout(() => {
expect(wrapper.find('.not-seen').length).toEqual(0);
done();
});
});
});
});
} catch (e) {
done();
}
it('full flow', () => {
const data = {url: '/notification_recipients'};
const wrapper = mount(<Notifications data={data} store={getStore()} />);
wrapper.find('.fa-bell').simulate('click');
expect(wrapper.find('.panel-group').length).toEqual(1);
wrapper.find('.panel-group .panel-heading').simulate('click');
expect(wrapper.find('.not-seen').length).toEqual(1);
wrapper.find('.not-seen').simulate('click');
expect(wrapper.find('.not-seen').length).toEqual(0);
});
it('mark group as read flow', () => {
const data = {url: '/notification_recipients'};
const wrapper = mount(<Notifications data={data} store={getStore()} />);
const matcher = '.drawer-pf-action a.btn-link';
wrapper.find('.fa-bell').simulate('click');
wrapper.find('.panel-group .panel-heading').simulate('click');
expect(wrapper.find(matcher).length).toBe(1);
expect(wrapper.find(`${matcher}[disabled=true]`).length).toBe(0);
wrapper.find(matcher).simulate('click');
expect(wrapper.find(`${matcher}[disabled=true]`).length).toBe(1);
});
});
webpack/assets/javascripts/react_app/redux/actions/notifications/index.js
NOTIFICATIONS_GET_NOTIFICATIONS,
NOTIFICATIONS_TOGGLE_DRAWER,
NOTIFICATIONS_SET_EXPANDED_GROUP,
NOTIFICATIONS_MARK_AS_READ
NOTIFICATIONS_MARK_AS_READ,
NOTIFICATIONS_MARK_GROUP_AS_READ
} from '../../consts';
import {
notificationsDrawer as sessionStorage
......
API.markNotificationAsRead(id);
};
export const onMarkGroupAsRead = (group) => dispatch => {
dispatch({
type: NOTIFICATIONS_MARK_GROUP_AS_READ,
payload: {
group
}
});
API.markGroupNotificationAsRead(group);
};
export const expandGroup = group => (dispatch, getState) => {
const currentExpanded = getState().notifications.expandedGroup;
webpack/assets/javascripts/react_app/redux/consts.js
'NOTIFICATIONS_SET_EXPANDED_GROUP';
export const NOTIFICATIONS_MARK_AS_READ =
'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_MARK_GROUP_AS_READ =
'NOTIFICATIONS_MARK_GROUP_AS_READ';
webpack/assets/javascripts/react_app/redux/index.js
let middleware = [thunk];
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && !global.__testing__) {
middleware = [...middleware, createLogger()];
}
export default createStore(
const _getStore = () => createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(...middleware)
);
export default _getStore();
export const getStore = _getStore;
webpack/assets/javascripts/react_app/redux/reducers/notifications/index.js
NOTIFICATIONS_GET_NOTIFICATIONS,
NOTIFICATIONS_TOGGLE_DRAWER,
NOTIFICATIONS_SET_EXPANDED_GROUP,
NOTIFICATIONS_MARK_AS_READ
NOTIFICATIONS_MARK_AS_READ,
NOTIFICATIONS_MARK_GROUP_AS_READ
} from '../../consts';
import Immutable from 'seamless-immutable';
import { notificationsDrawer } from '../../../common/sessionStorage';
......
case NOTIFICATIONS_SET_EXPANDED_GROUP:
return state.set('expandedGroup', payload.group);
case NOTIFICATIONS_MARK_AS_READ:
return state.set(
'notifications',
state.notifications.map(
n => n.id === payload.id ?
Object.assign({}, n, {seen: true}) : n
)
);
case NOTIFICATIONS_MARK_GROUP_AS_READ:
return state.set(
'notifications',
state.notifications.map(
n => n.id === payload.id ?
Object.assign({}, n, {seen: true}) :
n
n => n.group === payload.group ?
Object.assign({}, n, {seen: true}) : n
)
);
default:
webpack/assets/javascripts/react_app/redux/reducers/notifications/notifications.fixtures.js
/* eslint-disable camelcase */
import Immutable from 'seamless-immutable';
export const initialState = Immutable(
{
expandedGroup: null,
isDrawerOpen: null
});
export const stateWithNotifications = Immutable(
{
isDrawerOpen: true,
expandedGroup: 'Hosts',
notifications: [
{
id: 52435,
seen: false,
level: 'info',
text: 'some.example.com has been deleted successfully',
created_at: '2017-04-17T17:29:12.664Z',
group: 'Hosts',
actions: {}
},
{
id: 51435,
seen: false,
level: 'info',
text: 'notified successfully',
created_at: '2017-04-17T17:29:12.664Z',
group: 'Hosts',
actions: {}
},
{
id: 52433,
seen: false,
level: 'info',
text: 'notfied successfully',
created_at: '2017-04-17T17:29:12.664Z',
group: 'Testing',
actions: {}
}
]
}
);
export const request = {
group: 'Hosts'
};
export const response = Immutable(
{
isDrawerOpen: true,
expandedGroup: 'Hosts',
notifications: [
{
id: 52435,
seen: true,
level: 'info',
text: 'some.example.com has been deleted successfully',
created_at: '2017-04-17T17:29:12.664Z',
group: 'Hosts',
actions: {}
},
{
id: 51435,
seen: true,
level: 'info',
text: 'notified successfully',
created_at: '2017-04-17T17:29:12.664Z',
group: 'Hosts',
actions: {}
},
{
id: 52433,
seen: false,
level: 'info',
text: 'notfied successfully',
created_at: '2017-04-17T17:29:12.664Z',
group: 'Testing',
actions: {}
}
]
}
);
webpack/assets/javascripts/react_app/redux/reducers/notifications/notifications.test.js
import reducer from './index';
import * as types from '../../consts';
import {
initialState,
stateWithNotifications,
request,
response
} from './notifications.fixtures';
describe('notification reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});
it('should handle NOTIFICATIONS_MARK_GROUP_AS_READ', () => {
expect(
reducer(stateWithNotifications, {
type: types.NOTIFICATIONS_MARK_GROUP_AS_READ,
payload: request
})
).toEqual(response);
});
});

Also available in: Unified diff