Revision 59cdda31
Added by Gilad Lekner about 6 years ago
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;
|
||
}
|
Also available in: Unified diff
fixes #23357 - Refactor Notification Drawer from patternfly-react