Skip to content

Commit 2d3d002

Browse files
Merge pull request #53 from JakeSidSmith/search-notifications
Search notifications
2 parents 57dfb1d + 75bc581 commit 2d3d002

File tree

13 files changed

+372
-29
lines changed

13 files changed

+372
-29
lines changed

main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ app.on('ready', function(){
6464
label: 'Paste',
6565
accelerator: 'Command+V',
6666
selector: 'paste:'
67+
},
68+
{
69+
label: 'Select All',
70+
accelerator: 'Command+A',
71+
selector: 'selectAll:'
6772
}
6873
]
6974
}];

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@
7171
"src/js/components/repository.js": true,
7272
"src/js/components/settings.js": true,
7373
"src/js/components/footer.js": true,
74+
"src/js/components/search-input.js": true,
7475
"src/js/stores/auth.js": true,
7576
"src/js/stores/notifications.js": true,
77+
"src/js/stores/search.js": true,
7678
"src/js/stores/settings.js": true
7779
},
7880
"unmockedModulePathPatterns": [

src/js/__tests__/components/footer.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ describe('Test for Footer', function () {
1111

1212
var Actions, Footer;
1313

14+
window.localStorage = {
15+
item: false,
16+
setItem: function (item) {
17+
this.item = item;
18+
},
19+
getItem: function () {
20+
return this.item;
21+
},
22+
clear: function () {
23+
this.item = false;
24+
}
25+
};
26+
1427
beforeEach(function () {
1528
// Mock Electron's window.require
1629
// and remote.require('shell')

src/js/__tests__/components/notifications.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ describe('Test for Notifications Component', function () {
8282
errors = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'errored');
8383
expect(errors.length).toBe(1);
8484

85+
expect(instance.areIn('ekonstantinidis/gitify', 'gitify')).toBeTruthy();
86+
expect(instance.areIn('ekonstantinidis/gitify', 'hello')).toBeFalsy();
87+
88+
instance.state.searchTerm = 'hello';
89+
var matches = instance.matchesSearchTerm(response[0]);
90+
expect(matches).toBeFalsy();
91+
92+
instance.state.searchTerm = 'gitify';
93+
matches = instance.matchesSearchTerm(response[0]);
94+
expect(matches).toBeTruthy();
95+
});
96+
97+
it('Should only render repos that match the search term', function () {
98+
AuthStore.authStatus = function () {
99+
return true;
100+
};
101+
102+
var instance = TestUtils.renderIntoDocument(<Notifications />);
103+
104+
var response = [[{
105+
'repository': {
106+
'full_name': 'ekonstantinidis/gitify',
107+
'owner': {
108+
'avatar_url': 'http://avatar.url'
109+
}
110+
},
111+
'subject': {
112+
'type': 'Issue'
113+
}
114+
}]];
115+
116+
NotificationsStore.trigger(response);
117+
118+
var notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'repository');
119+
expect(notifications.length).toBe(1);
120+
121+
instance.state.searchTerm = 'hello';
122+
instance.forceUpdate();
123+
124+
notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'repository');
125+
expect(notifications.length).toBe(0);
85126
});
86127

87128
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* global jest, describe, beforeEach, it, expect, spyOn */
2+
3+
jest.dontMock('reflux');
4+
jest.dontMock('../../actions/actions.js');
5+
jest.dontMock('../../components/search-input.js');
6+
jest.dontMock('../../stores/auth.js');
7+
8+
var React = require('react/addons');
9+
var TestUtils = React.addons.TestUtils;
10+
11+
describe('Test for Search Input Component', function () {
12+
13+
var Actions, AuthStore, SearchInput;
14+
15+
beforeEach(function () {
16+
// Mock Electron's window.require
17+
// and remote.require('shell')
18+
window.require = function () {
19+
return {
20+
require: function () {
21+
return {
22+
openExternal: function () {
23+
return {};
24+
}
25+
};
26+
}
27+
};
28+
};
29+
30+
// Mock localStorage
31+
window.localStorage = {
32+
item: false,
33+
getItem: function () {
34+
return this.item;
35+
}
36+
};
37+
38+
Actions = require('../../actions/actions.js');
39+
AuthStore = require('../../stores/auth.js');
40+
SearchInput = require('../../components/search-input.js');
41+
});
42+
43+
it('Should make a search', function () {
44+
45+
spyOn(Actions, 'updateSearchTerm');
46+
spyOn(Actions, 'clearSearchTerm');
47+
48+
var instance = TestUtils.renderIntoDocument(<SearchInput />);
49+
50+
var wrapper = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'search-wrapper');
51+
expect(wrapper.length).toBe(1);
52+
53+
instance.clearSearch();
54+
55+
instance.onChange({
56+
target: {
57+
value: 'hello'
58+
}
59+
});
60+
61+
expect(Actions.updateSearchTerm).toHaveBeenCalledWith('hello');
62+
});
63+
64+
it('Should clear the search', function () {
65+
spyOn(Actions, 'clearSearchTerm');
66+
67+
var instance = TestUtils.renderIntoDocument(<SearchInput />);
68+
expect(Actions.clearSearchTerm).not.toHaveBeenCalled();
69+
70+
instance.clearSearch();
71+
expect(Actions.clearSearchTerm).toHaveBeenCalled();
72+
});
73+
74+
it('Should only render clear button if search term is not empty', function () {
75+
var instance = TestUtils.renderIntoDocument(<SearchInput />);
76+
77+
var clearButton = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'octicon-x');
78+
expect(clearButton.length).toBe(0);
79+
80+
instance.state.searchTerm = 'hello';
81+
instance.forceUpdate();
82+
83+
clearButton = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'octicon-x');
84+
expect(clearButton.length).toBe(1);
85+
});
86+
87+
});

src/js/__tests__/stores/search.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*global jest, describe, it, expect, beforeEach */
2+
3+
'use strict';
4+
5+
jest.dontMock('reflux');
6+
jest.dontMock('../../stores/search');
7+
jest.dontMock('../../actions/actions');
8+
9+
describe('Tests for AuthStore', function () {
10+
11+
var SearchStore, Actions;
12+
13+
beforeEach(function () {
14+
Actions = require('../../actions/actions');
15+
SearchStore = require('../../stores/search');
16+
});
17+
18+
it('should login - store the token', function () {
19+
var searchTerm = 'test';
20+
SearchStore.onUpdateSearchTerm(searchTerm);
21+
expect(SearchStore._searchTerm).toEqual(searchTerm);
22+
expect(SearchStore.searchTerm()).toEqual(searchTerm);
23+
});
24+
25+
it('should logout - remove the token', function () {
26+
SearchStore.onClearSearchTerm();
27+
expect(SearchStore._searchTerm).toEqual(undefined);
28+
expect(SearchStore.searchTerm()).toEqual(undefined);
29+
});
30+
31+
});

src/js/actions/actions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ var Actions = Reflux.createActions({
55
'login': {},
66
'logout': {},
77
'getNotifications': {asyncResult: true},
8+
'updateSearchTerm': {},
9+
'clearSearchTerm': {},
810
'setSetting': {}
911

1012
});

src/js/components/footer.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
var React = require('react');
2+
var Reflux = require('reflux');
23
var remote = window.require('remote');
34
var shell = remote.require('shell');
5+
var SearchInput = require('./search-input');
6+
var AuthStore = require('../stores/auth');
47

58
var Footer = React.createClass({
9+
mixins: [
10+
Reflux.connect(AuthStore, 'authStatus')
11+
],
612

713
openRepoBrowser: function () {
814
shell.openExternal('http://www.github.com/ekonstantinidis/gitify');
915
},
1016

17+
getInitialState: function () {
18+
return {
19+
authStatus: AuthStore.authStatus()
20+
};
21+
},
22+
1123
render: function () {
1224
return (
1325
<div className='container-fluid footer'>
1426
<div className='row'>
15-
<div
16-
className='col-xs-12 right'
17-
onClick={this.openRepoBrowser}>
27+
<div className="col-xs-6">
28+
{
29+
this.state.authStatus ? (
30+
<SearchInput />
31+
) : undefined
32+
}
33+
</div>
34+
<div className='col-xs-6 right'>
35+
<span className='github-link' onClick={this.openRepoBrowser}>
1836
Fork me on <span className="octicon octicon-mark-github"/>
19-
</div>
37+
</span>
38+
</div>
2039
</div>
2140
</div>
2241
);

src/js/components/notifications.js

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,28 @@ var React = require('react');
22
var Reflux = require('reflux');
33
var Loading = require('reloading');
44
var _ = require('underscore');
5-
var remote = window.require('remote');
6-
var shell = remote.require('shell');
75

86
var Actions = require('../actions/actions');
97
var NotificationsStore = require('../stores/notifications');
8+
var SearchStore = require('../stores/search');
109
var Repository = require('../components/repository');
1110

1211
var Notifications = React.createClass({
12+
areIn: function (repoFullName, searchTerm) {
13+
return repoFullName.toLowerCase().indexOf(searchTerm.toLowerCase()) >= 0;
14+
},
15+
16+
matchesSearchTerm: function (obj) {
17+
var repoFullName = obj[0].repository.full_name;
18+
var searchTerm = this.state.searchTerm.replace(/^\s+/, '').replace(/\s+$/, '');
19+
var searchTerms = searchTerm.split(/\s+/);
20+
21+
return _.all(searchTerms, this.areIn.bind(null, repoFullName));
22+
},
23+
1324
mixins: [
1425
Reflux.connect(NotificationsStore, 'notifications'),
26+
Reflux.connect(SearchStore, 'searchTerm'),
1527
Reflux.listenTo(Actions.getNotifications.completed, 'completedNotifications'),
1628
Reflux.listenTo(Actions.getNotifications.failed, 'failedNotifications')
1729
],
@@ -45,9 +57,9 @@ var Notifications = React.createClass({
4557
render: function () {
4658
var notifications, errors;
4759
var wrapperClass = 'container-fluid main-container notifications';
60+
var notificationsEmpty = _.isEmpty(this.state.notifications);
4861

4962
if (this.state.errors) {
50-
wrapperClass += ' errored';
5163
errors = (
5264
<div>
5365
<h3>Oops something went wrong.</h3>
@@ -56,8 +68,7 @@ var Notifications = React.createClass({
5668
</div>
5769
);
5870
} else {
59-
if (_.isEmpty(this.state.notifications)) {
60-
wrapperClass += ' all-read';
71+
if (notificationsEmpty) {
6172
notifications = (
6273
<div>
6374
<h2>There are no notifications for you.</h2>
@@ -66,17 +77,38 @@ var Notifications = React.createClass({
6677
</div>
6778
);
6879
} else {
69-
notifications = (
70-
this.state.notifications.map(function (obj) {
71-
var repoFullName = obj[0].repository.full_name;
72-
return <Repository repo={obj} repoName={repoFullName} key={repoFullName} />;
73-
})
74-
);
80+
if (this.state.searchTerm) {
81+
notifications = _.filter(this.state.notifications, this.matchesSearchTerm);
82+
} else {
83+
notifications = this.state.notifications;
84+
}
85+
86+
if (notifications.length) {
87+
notifications = (
88+
notifications.map(function (obj) {
89+
var repoFullName = obj[0].repository.full_name;
90+
return <Repository repo={obj} repoName={repoFullName} key={repoFullName} />;
91+
})
92+
);
93+
} else {
94+
notificationsEmpty = true;
95+
errors = (
96+
<div>
97+
<h3>No Search Results.</h3>
98+
<h4>No Organisations or Repositories match your search term.</h4>
99+
<img className='img-responsive emoji' src='images/all-read.png' />
100+
</div>
101+
);
102+
}
75103
}
76104
}
77105

78106
return (
79-
<div className={wrapperClass}>
107+
<div className={
108+
wrapperClass +
109+
(this.state.errors ? ' errored' : '') +
110+
(notificationsEmpty ? ' all-read' : '')
111+
}>
80112
<Loading className='loading-container' shouldShow={this.state.loading}>
81113
<div className='loading-text'>working on it</div>
82114
</Loading>

src/js/components/repository.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
var React = require('react');
2-
var _ = require('underscore');
32
var remote = window.require('remote');
43
var shell = remote.require('shell');
54

@@ -24,7 +23,7 @@ var Repository = React.createClass({
2423
<div className='col-xs-10 name' onClick={this.openBrowser}>{this.props.repoName}</div>
2524
</div>
2625

27-
{this.props.repo.map(function (obj, i) {
26+
{this.props.repo.map(function (obj) {
2827
return <SingleNotification notification={obj} key={obj.id} />;
2928
})}
3029

0 commit comments

Comments
 (0)