Project

General

Profile

« Previous | Next » 

Revision 59cdda31

Added by Gilad Lekner about 6 years ago

fixes #23357 - Refactor Notification Drawer from patternfly-react

View differences:

app/controllers/notification_recipients_controller.rb
head (count.zero? ? :not_modified : :ok)
end
def destroy_group
count = NotificationRecipient.
joins(:notification_blueprint).
where(user_id: User.current.id,
notification_blueprints: { group: params[:group]}).
delete_all
logger.debug("deleted #{count} notification recipents for group #{params[:group]}")
UINotifications::CacheHandler.new(User.current.id).clear unless count.zero?
head (count.zero? ? :not_modified : :ok)
end
private
def require_login
app/registries/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, :update_group_as_read] }, :public => true
:notification_recipients => [:index, :update, :destroy, :update_group_as_read, :destroy_group ] }, :public => true
map.permission :api_status, { :"api/v2/home" => [:status]}, :public => true
map.permission :about_index, { :about => [:index] }, :public => true
end
config/routes.rb
resources :notification_recipients, :only => [:index, :update, :destroy] do
collection do
put 'group/:group' => 'notification_recipients#update_group_as_read'
delete 'group/:group' => 'notification_recipients#destroy_group'
end
end
end
package.json
"lodash": "^4.15.0",
"multiselect": "~0.9.12",
"patternfly": "^3.42.0",
"patternfly-react": "^2.1.0",
"patternfly-react": "^2.3.3",
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-bootstrap": "^0.32.1",
test/controllers/notification_recipients_controller_test.rb
where(notification_blueprints: { group: 'Group2' }).count
end
test 'delete group notifications' do
add_notification
query = {user_id: User.current.id}
assert_equal 1, NotificationRecipient.where(query).count
delete :destroy_group, params: { :group => 'Testing' }, session: set_session_user
assert_response :success
assert_equal 0, NotificationRecipient.where(query).count
end
test 'group delete when multiple groups exists' do
add_notification('Group1')
add_notification('Group2')
query = {user_id: User.current.id}
assert_equal 2, NotificationRecipient.where(query).count
delete :destroy_group, params: { :group => 'Group1' }, session: set_session_user
assert_response :success
assert_equal 1, NotificationRecipient.where(query).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(group = 'Testing')
webpack/assets/javascripts/react_app/common/colors.scss
$pf-green-400: #3f9c35;
$pf-green-600: #1e4f18;
$pf-blue-400: #0088ce;
$pf-color-white: #fff;
$pf-border-gray: #d1d1d1;
$switcher-max-height: 300px;
$switcher-max-width: 200px;
webpack/assets/javascripts/react_app/components/common/Icon/Icon.test.js
jest.unmock('./');
describe('Icon', () => {
it('displays icon css', () => {
it('displays ok icon css', () => {
const wrapper = shallow(<Icon type="ok" />);
expect(wrapper.html()).toEqual('<span class="pficon pficon-ok"></span>');
});
it('can receive additionl css classes', () => {
const wrapper = shallow(<Icon type="ok" className="pull-left" />);
it('displays info icon css', () => {
const wrapper = shallow(<Icon type="info" />);
expect(wrapper.html()).toEqual('<span class="pficon pficon-ok pull-left"></span>');
expect(wrapper.html()).toEqual('<span class="pficon pficon-info"></span>');
});
it('displays warning icon css', () => {
const wrapper = shallow(<Icon type="warning" />);
expect(wrapper.html()).toEqual('<span class="pficon pficon-warning-triangle-o"></span>');
});
it('displays error icon css', () => {
const wrapper = shallow(<Icon type="error" />);
expect(wrapper.html()).toEqual('<span class="pficon pficon-error-circle-o"></span>');
});
it('displays close icon css', () => {
const wrapper = shallow(<Icon type="close" />);
expect(wrapper.html()).toEqual('<span class="pficon pficon-close"></span>');
});
});
webpack/assets/javascripts/react_app/components/notifications/__snapshots__/notifications.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`notifications empty state 1`] = `
<OnClickOutside(notificationContainer)
eventTypes={
Array [
"mousedown",
"touchstart",
]
}
excludeScrollbar={false}
expandGroup={[Function]}
isReady={false}
notifications={Object {}}
onClickedLink={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
outsideClickIgnoreClass="ignore-react-onclickoutside"
preventDefault={false}
startNotificationsPolling={[Function]}
stopPropagation={false}
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
storeSubscription={
Subscription {
"listeners": Object {
"clear": [Function],
"get": [Function],
"notify": [Function],
"subscribe": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": [Function],
}
}
toggleDrawer={[Function]}
/>
`;
exports[`notifications should close the notification box when click the close button 1`] = `
<Connect(OnClickOutside(notificationContainer))
data={
Object {
"url": "/notification_recipients",
}
}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<OnClickOutside(notificationContainer)
data={
Object {
"url": "/notification_recipients",
}
}
eventTypes={
Array [
"mousedown",
"touchstart",
]
}
excludeScrollbar={false}
expandGroup={[Function]}
expandedGroup={null}
hasUnreadMessages={true}
isDrawerOpen={true}
isPolling={true}
isReady={true}
notifications={
Object {
"React devs": Array [
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 1,
"level": "info",
"seen": true,
"text": null,
},
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 2,
"level": "info",
"seen": false,
"text": null,
},
],
}
}
onClickedLink={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
outsideClickIgnoreClass="ignore-react-onclickoutside"
preventDefault={false}
startNotificationsPolling={[Function]}
stopPropagation={false}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
storeSubscription={
Subscription {
"listeners": Object {
"clear": [Function],
"get": [Function],
"notify": [Function],
"subscribe": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
},
"unsubscribe": [Function],
}
}
toggleDrawer={[Function]}
>
<notificationContainer
data={
Object {
"url": "/notification_recipients",
}
}
disableOnClickOutside={[Function]}
enableOnClickOutside={[Function]}
eventTypes={
Array [
"mousedown",
"touchstart",
]
}
expandGroup={[Function]}
expandedGroup={null}
hasUnreadMessages={true}
isDrawerOpen={true}
isPolling={true}
isReady={true}
notifications={
Object {
"React devs": Array [
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 1,
"level": "info",
"seen": true,
"text": null,
},
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 2,
"level": "info",
"seen": false,
"text": null,
},
],
}
}
onClickedLink={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
outsideClickIgnoreClass="ignore-react-onclickoutside"
preventDefault={false}
startNotificationsPolling={[Function]}
stopPropagation={false}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
storeSubscription={
Subscription {
"listeners": Object {
"clear": [Function],
"get": [Function],
"notify": [Function],
"subscribe": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
},
"unsubscribe": [Function],
}
}
toggleDrawer={[Function]}
>
<div>
<Component
hasUnreadMessages={true}
onClick={[Function]}
>
<OverlayTrigger
defaultOverlayShown={false}
id="notifications-toggle-icon"
overlay={
<Tooltip
bsClass="tooltip"
id="tooltip"
placement="right"
>
Notifications
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<span
aria-describedby="tooltip"
className="fa fa-bell"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
/>
</OverlayTrigger>
</Component>
<Component
expandedGroup={null}
notificationGroups={
Object {
"React devs": Array [
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 1,
"level": "info",
"seen": true,
"text": null,
},
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 2,
"level": "info",
"seen": false,
"text": null,
},
],
}
}
onClickedLink={[Function]}
onExpandGroup={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
toggleDrawer={[Function]}
>
<div
className="drawer-pf drawer-pf-notifications-non-clickable"
>
<div
className="drawer-pf-title"
>
<a
className="drawer-pf-close pficon pficon-close"
onClick={[Function]}
/>
<h3
className="text-center"
>
Notifications
</h3>
</div>
<div
className="panel-group"
id="notification-drawer-accordion"
>
<Component
group="React devs"
isExpanded={false}
key="React devs"
notifications={
Array [
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 1,
"level": "info",
"seen": true,
"text": null,
},
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 2,
"level": "info",
"seen": false,
"text": null,
},
]
}
onClickedLink={[Function]}
onExpand={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
>
<div
className="panel panel-default "
>
<div
className="panel-heading"
onClick={[Function]}
>
<h4
className="panel-title"
>
<a
className="collapsed"
>
React devs
</a>
</h4>
<span
className="panel-counter"
>
1 New Event
</span>
</div>
</div>
</Component>
</div>
</div>
</Component>
</div>
</notificationContainer>
</OnClickOutside(notificationContainer)>
</Connect(OnClickOutside(notificationContainer))>
`;
exports[`notifications should close the notification box when click the close button 2`] = `
<Connect(OnClickOutside(notificationContainer))
data={
Object {
"url": "/notification_recipients",
}
}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<OnClickOutside(notificationContainer)
data={
Object {
"url": "/notification_recipients",
}
}
eventTypes={
Array [
"mousedown",
"touchstart",
]
}
excludeScrollbar={false}
expandGroup={[Function]}
expandedGroup={null}
hasUnreadMessages={true}
isDrawerOpen={false}
isPolling={true}
isReady={true}
notifications={
Object {
"React devs": Array [
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 1,
"level": "info",
"seen": true,
"text": null,
},
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 2,
"level": "info",
"seen": false,
"text": null,
},
],
}
}
onClickedLink={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
outsideClickIgnoreClass="ignore-react-onclickoutside"
preventDefault={false}
startNotificationsPolling={[Function]}
stopPropagation={false}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
storeSubscription={
Subscription {
"listeners": Object {
"clear": [Function],
"get": [Function],
"notify": [Function],
"subscribe": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
},
"unsubscribe": [Function],
}
}
toggleDrawer={[Function]}
>
<notificationContainer
data={
Object {
"url": "/notification_recipients",
}
}
disableOnClickOutside={[Function]}
enableOnClickOutside={[Function]}
eventTypes={
Array [
"mousedown",
"touchstart",
]
}
expandGroup={[Function]}
expandedGroup={null}
hasUnreadMessages={true}
isDrawerOpen={false}
isPolling={true}
isReady={true}
notifications={
Object {
"React devs": Array [
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 1,
"level": "info",
"seen": true,
"text": null,
},
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 2,
"level": "info",
"seen": false,
"text": null,
},
],
}
}
onClickedLink={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
outsideClickIgnoreClass="ignore-react-onclickoutside"
preventDefault={false}
startNotificationsPolling={[Function]}
stopPropagation={false}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
storeSubscription={
Subscription {
"listeners": Object {
"clear": [Function],
"get": [Function],
"notify": [Function],
"subscribe": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
},
"unsubscribe": [Function],
}
}
toggleDrawer={[Function]}
>
<div>
<Component
hasUnreadMessages={true}
onClick={[Function]}
>
<OverlayTrigger
defaultOverlayShown={false}
id="notifications-toggle-icon"
overlay={
<Tooltip
bsClass="tooltip"
id="tooltip"
placement="right"
>
Notifications
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<span
aria-describedby="tooltip"
className="fa fa-bell"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
/>
</OverlayTrigger>
</Component>
</div>
</notificationContainer>
</OnClickOutside(notificationContainer)>
</Connect(OnClickOutside(notificationContainer))>
`;
exports[`notifications should render empty html for state before notifications 1`] = `
<OnClickOutside(notificationContainer)
eventTypes={
Array [
"mousedown",
"touchstart",
]
}
excludeScrollbar={false}
expandGroup={[Function]}
expandedGroup="React devs2"
isDrawerOpen={true}
isReady={false}
notifications={Object {}}
onClickedLink={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
outsideClickIgnoreClass="ignore-react-onclickoutside"
preventDefault={false}
startNotificationsPolling={[Function]}
stopPropagation={false}
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
storeSubscription={
Subscription {
"listeners": Object {
"clear": [Function],
"get": [Function],
"notify": [Function],
"subscribe": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": [Function],
}
}
toggleDrawer={[Function]}
/>
`;
exports[`notifications should render full html on a state with notifications 1`] = `
<OnClickOutside(notificationContainer)
eventTypes={
Array [
"mousedown",
"touchstart",
]
}
excludeScrollbar={false}
expandGroup={[Function]}
expandedGroup="React devs2"
hasUnreadMessages={true}
isDrawerOpen={true}
isReady={true}
notifications={
Object {
"React devs": Array [
Object {
"actions": Object {},
"created_at": "2017-02-23T05:00:28.715Z",
"group": "React devs",
"id": 1,
"level": "info",
"seen": true,
"text": null,
},
],
"React devs2": Array [
Object {
"actions": Object {
"links": Array [
Object {
"href": "https://theforeman.org/blog",
"title": "Link to blog",
},
],
},
"created_at": "2017-03-14T11:25:07.138Z",
"group": "React devs2",
"id": 6,
"level": "info",
"seen": true,
"text": "Hi! This is a notification message",
},
],
}
}
onClickedLink={[Function]}
onMarkAsRead={[Function]}
onMarkGroupAsRead={[Function]}
outsideClickIgnoreClass="ignore-react-onclickoutside"
preventDefault={false}
startNotificationsPolling={[Function]}
stopPropagation={false}
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
storeSubscription={
Subscription {
"listeners": Object {
"clear": [Function],
"get": [Function],
"notify": [Function],
"subscribe": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": [Function],
}
}
toggleDrawer={[Function]}
/>
`;
webpack/assets/javascripts/react_app/components/notifications/drawer/NotificationDropdown.fixtures.js
export const propsWithLinks = {
id: 6,
links: [
{
href: 'https://theforeman.org/blog',
title: 'Link to blog',
},
],
onClickedLink: () => {},
};
webpack/assets/javascripts/react_app/components/notifications/drawer/NotificationDropdown.js
import { DropdownKebab, MenuItem } from 'patternfly-react';
import React from 'react';
const NotificationDropdown = ({ links, id, onClickedLink }) => (
<DropdownKebab pullRight id={id}>
{links.map((link, i) => (
<MenuItem key={i} id={`notification-kebab-${i}`} onClick={onClickedLink.bind(this, link)}>
{link.title}
</MenuItem>
))}
</DropdownKebab>
);
export default NotificationDropdown;
webpack/assets/javascripts/react_app/components/notifications/drawer/NotificationDropdown.test.js
import toJson from 'enzyme-to-json';
import { shallow } from 'enzyme';
import React from 'react';
import NotificationDropdown from './NotificationDropdown';
import { propsWithLinks } from './NotificationDropdown.fixtures';
describe('Notification dropdown', () => {
it('Renders links provided', () => {
const wrapper = shallow(<NotificationDropdown {...propsWithLinks} />);
expect(toJson(wrapper)).toMatchSnapshot();
});
});
webpack/assets/javascripts/react_app/components/notifications/drawer/__snapshots__/NotificationDropdown.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Notification dropdown Renders links provided 1`] = `
<DropdownKebab
className=""
componentClass={[Function]}
id={6}
pullRight={true}
toggleStyle="link"
>
<MenuItem
bsClass="dropdown"
disabled={false}
divider={false}
header={false}
id="notification-kebab-0"
key="0"
onClick={[Function]}
>
Link to blog
</MenuItem>
</DropdownKebab>
`;
webpack/assets/javascripts/react_app/components/notifications/drawer/index.js
import React from 'react';
import NotificationsGroup from './notificationGroup';
export default ({
notificationGroups,
expandedGroup,
toggleDrawer,
onExpandGroup,
onMarkAsRead,
onMarkGroupAsRead,
onClickedLink,
}) => {
const groups = Object.keys(notificationGroups)
.map(key => (
<NotificationsGroup
group={key}
key={key}
onClickedLink={onClickedLink}
onMarkAsRead={onMarkAsRead}
onMarkGroupAsRead={onMarkGroupAsRead}
isExpanded={expandedGroup === key}
onExpand={onExpandGroup}
notifications={notificationGroups[key]}
/>
));
const noNotificationsMessage = (
<div id="no-notifications-container">
{__('No Notifications')}
</div>
);
return (
<div className="drawer-pf drawer-pf-notifications-non-clickable">
<div className="drawer-pf-title">
<a className="drawer-pf-close pficon pficon-close" onClick={toggleDrawer} />
<h3 className="text-center">{__('Notifications')}</h3>
</div>
<div className="panel-group" id="notification-drawer-accordion">
{groups.length === 0 ? noNotificationsMessage : groups}
</div>
</div>
);
};
webpack/assets/javascripts/react_app/components/notifications/drawer/notification.js
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import React from 'react';
import Icon from '../../common/Icon';
import '../../../common/commonStyles.css';
import NotificationDropdown from './NotificationDropdown';
/* eslint-disable camelcase */
const Notification = ({
notification: {
created_at,
seen,
text,
level,
id,
actions,
},
onMarkAsRead,
onClickedLink,
}) => {
const created = new Date(created_at);
const title = __('Click to mark as read');
const tooltip = (
<Tooltip id="tooltip">{ title }</Tooltip>
);
const messageText = seen ?
<span className="drawer-pf-notification-message">{text}</span> :
(<span
className="drawer-pf-notification-message not-seen"
onClick={onMarkAsRead.bind(this, id)}
>
<OverlayTrigger placement="top" overlay={tooltip}>
<span>{ text }</span>
</OverlayTrigger>
</span>);
return (
<div className="drawer-pf-notification">
<Icon type={level} />
<div className="notification-text-container">
{messageText}
<div className="drawer-pf-notification-info">
<span className="date">{created.toLocaleDateString()}</span>
<span className="time">{created.toLocaleTimeString()}</span>
</div>
</div>
{
actions.links &&
<NotificationDropdown
links={actions.links}
id={id}
onClickedLink={onClickedLink}
/>
}
</div>
);
};
export default Notification;
webpack/assets/javascripts/react_app/components/notifications/drawer/notificationGroup.js
import React from 'react';
import Notification from './notification';
export default ({
group,
notifications,
isExpanded,
onExpand,
onMarkAsRead,
onMarkGroupAsRead,
onClickedLink,
}) => {
const className = `panel panel-default ${isExpanded ? 'expanded' : ''}`;
const unreadCount = notifications.filter(notification => !notification.seen).length;
return (
<div className={className}>
<div className="panel-heading" onClick={() => onExpand(group)}>
<h4 className="panel-title">
<a className={isExpanded ? '' : 'collapsed'}>
{group}
</a>
</h4>
<span className="panel-counter">
{
`${unreadCount} ${unreadCount !== 1 ?
__('New Events') :
__('New Event')}`
}
</span>
</div>
{isExpanded &&
<div className="panel-body">
{ notifications.map(notification => (
<Notification
onClickedLink={onClickedLink}
key={notification.id}
notification={notification}
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
import { groupBy } from 'lodash';
import onClickOutside from 'react-onclickoutside';
import { connect } from 'react-redux';
import React from 'react';
import { connect } from 'react-redux';
import { groupBy } from 'lodash';
import { NotificationDrawerWrapper } from 'patternfly-react';
import * as NotificationActions from '../../redux/actions/notifications';
import './notifications.scss';
import ToggleIcon from './toggleIcon/';
import Drawer from './drawer/';
class notificationContainer extends React.Component {
componentDidMount() {
......
toggleDrawer,
expandGroup,
expandedGroup,
onMarkAsRead,
onMarkGroupAsRead,
markAsRead,
markGroupAsRead,
clearNotification,
clearGroup,
hasUnreadMessages,
isReady,
onClickedLink,
clickedLink,
} = this.props;
const notificationGroups = Object.entries(notifications).map(([key, group]) => ({
panelkey: key,
panelName: key,
notifications: group,
}));
const translations = {
title: __('Notifications'),
unreadEvent: __('Unread Event'),
unreadEvents: __('Unread Events'),
emptyState: __('No Notifications Available'),
readAll: __('Mark All Read'),
clearAll: __('Clear All'),
deleteNotification: __('Hide this notification'),
};
return (
<div>
<ToggleIcon hasUnreadMessages={hasUnreadMessages} onClick={toggleDrawer} />
{isReady &&
isDrawerOpen && (
<Drawer
onExpandGroup={expandGroup}
onClickedLink={onClickedLink}
onMarkAsRead={onMarkAsRead}
onMarkGroupAsRead={onMarkGroupAsRead}
expandedGroup={expandedGroup}
notificationGroups={notifications}
toggleDrawer={toggleDrawer}
<NotificationDrawerWrapper
panels={notificationGroups}
expandedPanel={expandedGroup}
togglePanel={expandGroup}
onNotificationAsRead={markAsRead}
onNotificationHide={clearNotification}
onMarkPanelAsRead={markGroupAsRead}
onMarkPanelAsClear={clearGroup}
onClickedLink={clickedLink}
toggleDrawerHide={toggleDrawer}
isExpandable={false}
translations={translations}
/>
)}
</div>
webpack/assets/javascripts/react_app/components/notifications/notifications.fixtures.js
import immutable from 'seamless-immutable';
export const componentMountData = { url: '/notification_recipients' };
export const emptyState = immutable({
notifications: {},
});
export const stateWithoutNotifications = immutable({
notifications: {
expandedGroup: 'React devs2',
isDrawerOpen: true,
},
});
export const stateWithNotifications = immutable({
notifications: {
expandedGroup: 'React devs2',
isDrawerOpen: true,
notifications: {
1: {
id: 1,
seen: true,
level: 'info',
text: null,
created_at: '2017-02-23T05:00:28.715Z',
group: 'React devs',
actions: {},
},
6: {
id: 6,
seen: true,
level: 'info',
text: 'Hi! This is a notification message',
created_at: '2017-03-14T11:25:07.138Z',
group: 'React devs2',
actions: {
links: [
{
href: 'https://theforeman.org/blog',
title: 'Link to blog',
},
],
},
},
},
hasUnreadMessages: true,
},
});
export const stateWithUnreadNotifications = immutable({
notifications: {
expandedGroup: 'React devs2',
isDrawerOpen: true,
notifications: {
1: {
id: 1,
seen: true,
level: 'info',
text: null,
created_at: '2017-02-23T05:00:28.715Z',
group: 'React devs',
actions: {},
},
6: {
id: 6,
seen: false,
level: 'info',
text: 'Hi! This is a notification message',
created_at: '2017-03-14T11:25:07.138Z',
group: 'React devs2',
actions: {
links: [
{
href: 'https://theforeman.org/blog',
title: 'Link to blog',
},
],
},
},
},
hasUnreadMessages: true,
},
});
export const serverResponse = `{"data": { "notifications":[
{"id":1,"seen":true,"level":"info","text":null,"created_at":"2017-02-23T05:00:28.715Z",
{"id":1,"seen":true,"level":"info","text":"notification1","created_at":"2017-02-23T05:00:28.715Z",
"group":"React devs","actions":{}},
{"id":2,"seen":false,"level":"info","text":null,"created_at":"2017-02-23T05:00:28.715Z",
{"id":2,"seen":false,"level":"info","text":"notification2","created_at":"2017-02-23T05:00:28.715Z",
"group":"React devs","actions":{}}]}}`;
export const emptyHtml =
'<div id="notifications_container">' +
'<span class="fa fa-bell-o" aria-describedby="tooltip">' +
'</span></div>';
webpack/assets/javascripts/react_app/components/notifications/notifications.scss
@import '../../common/colors.scss';
#notifications_container {
position: static;
margin-top: -1px;
......
}
}
/*
Changed the maximum height of the drawer to full window, this prevents double scrolling as much as possible while still keeping a dynamic height.
Added flexbox functionality to resize the drawer if the window gets resized.
*/
.drawer-pf {
max-height: 600px;
max-height: calc(100vh - 58px - 20px);
overflow: hidden;
display: flex;
flex-direction: column;
.drawer-pf-title,
.panel-group {
position: static;
}
.drawer-pf-close {
right: 0;
}
.drawer-pf-toggle-expand {
left: 0;
.drawer-pf-title {
position: relative;
}
.panel-group {
flex: 1;
display: flex;
flex-direction: column;
position: initial;
bottom: initial;
top: initial;
overflow-y: auto;
.blank-slate-pf-title {
font-size: 18px;
}
.panel.expanded {
flex: 1;
.panel.panel-default.expanded {
flex: 1 1 auto;
display: flex;
flex-direction: column;
.panel-body {
overflow-y: auto;
flex: 1;
}
}
.panel {
cursor: pointer;
.panel-title {
a {
font-size: 14px;
}
}
.panel-collapse.in {
min-height: 0;
overflow-y: auto;
.panel-body {
padding: 0;
.drawer-pf-notification {
display: flex;
.notification-text-container {
display: flex;
flex: 1;
flex-direction: column;
position: relative;
margin: 0 12px;
.not-seen {
font-weight: bold;
cursor: pointer;
}
.drawer-pf-notification-message,
.drawer-pf-notification-info {
padding: 0;
}
display: block;
.dropdown-menu-right {
position: absolute;
.btn-group {
z-index: 1;
}
}
}
}
}
.drawer-pf-action {
padding: 10px 0;
position: relative;
position: sticky;
position: -webkit-sticky;
position: -moz-sticky;
position: -ms-sticky;
position: -o-sticky;
bottom: 0;
z-index: 10;
background-color: $pf-color-white;
border-top: 1px solid $pf-border-gray;
}
.drawer-pf-action-link {
align-self: center;
margin: 0;
}
}
#no-notifications-container {
padding: 20px;
text-align: center;
background: #efefef;
background: $pf-black-200;
font-size: 15px;
}
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff