diff --git a/main.js b/main.js
index c8e1bfec4..dcaddbc58 100644
--- a/main.js
+++ b/main.js
@@ -64,6 +64,11 @@ app.on('ready', function(){
label: 'Paste',
accelerator: 'Command+V',
selector: 'paste:'
+ },
+ {
+ label: 'Select All',
+ accelerator: 'Command+A',
+ selector: 'selectAll:'
}
]
}];
diff --git a/package.json b/package.json
index c044d96f4..277ff0f3e 100644
--- a/package.json
+++ b/package.json
@@ -71,8 +71,10 @@
"src/js/components/repository.js": true,
"src/js/components/settings.js": true,
"src/js/components/footer.js": true,
+ "src/js/components/search-input.js": true,
"src/js/stores/auth.js": true,
"src/js/stores/notifications.js": true,
+ "src/js/stores/search.js": true,
"src/js/stores/settings.js": true
},
"unmockedModulePathPatterns": [
diff --git a/src/js/__tests__/components/footer.js b/src/js/__tests__/components/footer.js
index e8516c2d5..cd160bed7 100644
--- a/src/js/__tests__/components/footer.js
+++ b/src/js/__tests__/components/footer.js
@@ -11,6 +11,19 @@ describe('Test for Footer', function () {
var Actions, Footer;
+ window.localStorage = {
+ item: false,
+ setItem: function (item) {
+ this.item = item;
+ },
+ getItem: function () {
+ return this.item;
+ },
+ clear: function () {
+ this.item = false;
+ }
+ };
+
beforeEach(function () {
// Mock Electron's window.require
// and remote.require('shell')
diff --git a/src/js/__tests__/components/notifications.js b/src/js/__tests__/components/notifications.js
index d2687b90e..7b69d6463 100644
--- a/src/js/__tests__/components/notifications.js
+++ b/src/js/__tests__/components/notifications.js
@@ -82,6 +82,47 @@ describe('Test for Notifications Component', function () {
errors = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'errored');
expect(errors.length).toBe(1);
+ expect(instance.areIn('ekonstantinidis/gitify', 'gitify')).toBeTruthy();
+ expect(instance.areIn('ekonstantinidis/gitify', 'hello')).toBeFalsy();
+
+ instance.state.searchTerm = 'hello';
+ var matches = instance.matchesSearchTerm(response[0]);
+ expect(matches).toBeFalsy();
+
+ instance.state.searchTerm = 'gitify';
+ matches = instance.matchesSearchTerm(response[0]);
+ expect(matches).toBeTruthy();
+ });
+
+ it('Should only render repos that match the search term', function () {
+ AuthStore.authStatus = function () {
+ return true;
+ };
+
+ var instance = TestUtils.renderIntoDocument();
+
+ var response = [[{
+ 'repository': {
+ 'full_name': 'ekonstantinidis/gitify',
+ 'owner': {
+ 'avatar_url': 'http://avatar.url'
+ }
+ },
+ 'subject': {
+ 'type': 'Issue'
+ }
+ }]];
+
+ NotificationsStore.trigger(response);
+
+ var notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'repository');
+ expect(notifications.length).toBe(1);
+
+ instance.state.searchTerm = 'hello';
+ instance.forceUpdate();
+
+ notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'repository');
+ expect(notifications.length).toBe(0);
});
});
diff --git a/src/js/__tests__/components/search-input.js b/src/js/__tests__/components/search-input.js
new file mode 100644
index 000000000..a0575e70e
--- /dev/null
+++ b/src/js/__tests__/components/search-input.js
@@ -0,0 +1,87 @@
+/* global jest, describe, beforeEach, it, expect, spyOn */
+
+jest.dontMock('reflux');
+jest.dontMock('../../actions/actions.js');
+jest.dontMock('../../components/search-input.js');
+jest.dontMock('../../stores/auth.js');
+
+var React = require('react/addons');
+var TestUtils = React.addons.TestUtils;
+
+describe('Test for Search Input Component', function () {
+
+ var Actions, AuthStore, SearchInput;
+
+ beforeEach(function () {
+ // Mock Electron's window.require
+ // and remote.require('shell')
+ window.require = function () {
+ return {
+ require: function () {
+ return {
+ openExternal: function () {
+ return {};
+ }
+ };
+ }
+ };
+ };
+
+ // Mock localStorage
+ window.localStorage = {
+ item: false,
+ getItem: function () {
+ return this.item;
+ }
+ };
+
+ Actions = require('../../actions/actions.js');
+ AuthStore = require('../../stores/auth.js');
+ SearchInput = require('../../components/search-input.js');
+ });
+
+ it('Should make a search', function () {
+
+ spyOn(Actions, 'updateSearchTerm');
+ spyOn(Actions, 'clearSearchTerm');
+
+ var instance = TestUtils.renderIntoDocument();
+
+ var wrapper = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'search-wrapper');
+ expect(wrapper.length).toBe(1);
+
+ instance.clearSearch();
+
+ instance.onChange({
+ target: {
+ value: 'hello'
+ }
+ });
+
+ expect(Actions.updateSearchTerm).toHaveBeenCalledWith('hello');
+ });
+
+ it('Should clear the search', function () {
+ spyOn(Actions, 'clearSearchTerm');
+
+ var instance = TestUtils.renderIntoDocument();
+ expect(Actions.clearSearchTerm).not.toHaveBeenCalled();
+
+ instance.clearSearch();
+ expect(Actions.clearSearchTerm).toHaveBeenCalled();
+ });
+
+ it('Should only render clear button if search term is not empty', function () {
+ var instance = TestUtils.renderIntoDocument();
+
+ var clearButton = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'octicon-x');
+ expect(clearButton.length).toBe(0);
+
+ instance.state.searchTerm = 'hello';
+ instance.forceUpdate();
+
+ clearButton = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'octicon-x');
+ expect(clearButton.length).toBe(1);
+ });
+
+});
diff --git a/src/js/__tests__/stores/search.js b/src/js/__tests__/stores/search.js
new file mode 100644
index 000000000..2fde6a084
--- /dev/null
+++ b/src/js/__tests__/stores/search.js
@@ -0,0 +1,31 @@
+/*global jest, describe, it, expect, beforeEach */
+
+'use strict';
+
+jest.dontMock('reflux');
+jest.dontMock('../../stores/search');
+jest.dontMock('../../actions/actions');
+
+describe('Tests for AuthStore', function () {
+
+ var SearchStore, Actions;
+
+ beforeEach(function () {
+ Actions = require('../../actions/actions');
+ SearchStore = require('../../stores/search');
+ });
+
+ it('should login - store the token', function () {
+ var searchTerm = 'test';
+ SearchStore.onUpdateSearchTerm(searchTerm);
+ expect(SearchStore._searchTerm).toEqual(searchTerm);
+ expect(SearchStore.searchTerm()).toEqual(searchTerm);
+ });
+
+ it('should logout - remove the token', function () {
+ SearchStore.onClearSearchTerm();
+ expect(SearchStore._searchTerm).toEqual(undefined);
+ expect(SearchStore.searchTerm()).toEqual(undefined);
+ });
+
+});
diff --git a/src/js/actions/actions.js b/src/js/actions/actions.js
index ac6e31857..d26dab6eb 100644
--- a/src/js/actions/actions.js
+++ b/src/js/actions/actions.js
@@ -5,6 +5,8 @@ var Actions = Reflux.createActions({
'login': {},
'logout': {},
'getNotifications': {asyncResult: true},
+ 'updateSearchTerm': {},
+ 'clearSearchTerm': {},
'setSetting': {}
});
diff --git a/src/js/components/footer.js b/src/js/components/footer.js
index 3aff9fd1e..89aa9f0e3 100644
--- a/src/js/components/footer.js
+++ b/src/js/components/footer.js
@@ -1,22 +1,41 @@
var React = require('react');
+var Reflux = require('reflux');
var remote = window.require('remote');
var shell = remote.require('shell');
+var SearchInput = require('./search-input');
+var AuthStore = require('../stores/auth');
var Footer = React.createClass({
+ mixins: [
+ Reflux.connect(AuthStore, 'authStatus')
+ ],
openRepoBrowser: function () {
shell.openExternal('http://www.github.com/ekonstantinidis/gitify');
},
+ getInitialState: function () {
+ return {
+ authStatus: AuthStore.authStatus()
+ };
+ },
+
render: function () {
return (
-
+
+ {
+ this.state.authStatus ? (
+
+ ) : undefined
+ }
+
+
+
Fork me on
-
+
+
);
diff --git a/src/js/components/notifications.js b/src/js/components/notifications.js
index 76d137d8f..3f0a32f89 100644
--- a/src/js/components/notifications.js
+++ b/src/js/components/notifications.js
@@ -2,16 +2,28 @@ var React = require('react');
var Reflux = require('reflux');
var Loading = require('reloading');
var _ = require('underscore');
-var remote = window.require('remote');
-var shell = remote.require('shell');
var Actions = require('../actions/actions');
var NotificationsStore = require('../stores/notifications');
+var SearchStore = require('../stores/search');
var Repository = require('../components/repository');
var Notifications = React.createClass({
+ areIn: function (repoFullName, searchTerm) {
+ return repoFullName.toLowerCase().indexOf(searchTerm.toLowerCase()) >= 0;
+ },
+
+ matchesSearchTerm: function (obj) {
+ var repoFullName = obj[0].repository.full_name;
+ var searchTerm = this.state.searchTerm.replace(/^\s+/, '').replace(/\s+$/, '');
+ var searchTerms = searchTerm.split(/\s+/);
+
+ return _.all(searchTerms, this.areIn.bind(null, repoFullName));
+ },
+
mixins: [
Reflux.connect(NotificationsStore, 'notifications'),
+ Reflux.connect(SearchStore, 'searchTerm'),
Reflux.listenTo(Actions.getNotifications.completed, 'completedNotifications'),
Reflux.listenTo(Actions.getNotifications.failed, 'failedNotifications')
],
@@ -45,9 +57,9 @@ var Notifications = React.createClass({
render: function () {
var notifications, errors;
var wrapperClass = 'container-fluid main-container notifications';
+ var notificationsEmpty = _.isEmpty(this.state.notifications);
if (this.state.errors) {
- wrapperClass += ' errored';
errors = (
Oops something went wrong.
@@ -56,8 +68,7 @@ var Notifications = React.createClass({
);
} else {
- if (_.isEmpty(this.state.notifications)) {
- wrapperClass += ' all-read';
+ if (notificationsEmpty) {
notifications = (
There are no notifications for you.
@@ -66,17 +77,38 @@ var Notifications = React.createClass({
);
} else {
- notifications = (
- this.state.notifications.map(function (obj) {
- var repoFullName = obj[0].repository.full_name;
- return ;
- })
- );
+ if (this.state.searchTerm) {
+ notifications = _.filter(this.state.notifications, this.matchesSearchTerm);
+ } else {
+ notifications = this.state.notifications;
+ }
+
+ if (notifications.length) {
+ notifications = (
+ notifications.map(function (obj) {
+ var repoFullName = obj[0].repository.full_name;
+ return ;
+ })
+ );
+ } else {
+ notificationsEmpty = true;
+ errors = (
+
+
No Search Results.
+
No Organisations or Repositories match your search term.
+

+
+ );
+ }
}
}
return (
-
+
working on it
diff --git a/src/js/components/repository.js b/src/js/components/repository.js
index a03807a83..1559ecc69 100644
--- a/src/js/components/repository.js
+++ b/src/js/components/repository.js
@@ -1,5 +1,4 @@
var React = require('react');
-var _ = require('underscore');
var remote = window.require('remote');
var shell = remote.require('shell');
@@ -24,7 +23,7 @@ var Repository = React.createClass({
{this.props.repoName}
- {this.props.repo.map(function (obj, i) {
+ {this.props.repo.map(function (obj) {
return
;
})}
diff --git a/src/js/components/search-input.js b/src/js/components/search-input.js
new file mode 100644
index 000000000..026408943
--- /dev/null
+++ b/src/js/components/search-input.js
@@ -0,0 +1,46 @@
+var React = require('react');
+var Reflux = require('reflux');
+var SearchStore = require('../stores/search');
+var Actions = require('../actions/actions');
+
+var SearchInput = React.createClass({
+ mixins: [
+ Reflux.connect(SearchStore, 'searchTerm')
+ ],
+
+ onChange: function (event) {
+ Actions.updateSearchTerm(event.target.value);
+ },
+
+ clearSearch: function () {
+ Actions.clearSearchTerm();
+ },
+
+ getInitialState: function () {
+ return {};
+ },
+
+ render: function () {
+ var clearSearchIcon;
+
+ if (this.state.searchTerm) {
+ clearSearchIcon = (
+
+ );
+ }
+
+ return (
+
+ {clearSearchIcon}
+
+
+ );
+ }
+});
+
+module.exports = SearchInput;
diff --git a/src/js/stores/search.js b/src/js/stores/search.js
new file mode 100644
index 000000000..ba5002a16
--- /dev/null
+++ b/src/js/stores/search.js
@@ -0,0 +1,22 @@
+var Reflux = require('reflux');
+var Actions = require('../actions/actions');
+
+var SearchStore = Reflux.createStore({
+ listenables: Actions,
+
+ onUpdateSearchTerm: function (searchTerm) {
+ this._searchTerm = searchTerm;
+ this.trigger(this.searchTerm());
+ },
+
+ onClearSearchTerm: function () {
+ this._searchTerm = undefined;
+ this.trigger(this.searchTerm());
+ },
+
+ searchTerm: function () {
+ return this._searchTerm;
+ }
+});
+
+module.exports = SearchStore;
diff --git a/src/less/style.less b/src/less/style.less
index dca4054c7..36bf4a5b3 100644
--- a/src/less/style.less
+++ b/src/less/style.less
@@ -111,16 +111,15 @@
/* @group Misc */
+html, body {
+ -webkit-user-select: none;
+}
+
body {
.FontOpenSansRegular();
cursor: default;
}
-::selection {
- color: inherit;
- background: inherit;
-}
-
html, body {
height: 100%;
overflow: hidden;
@@ -148,6 +147,14 @@ html, body {
}
}
+.right {
+ text-align: right;
+}
+
+input {
+ outline: none;
+}
+
/* @end Misc */
@@ -214,12 +221,8 @@ html, body {
}
}
- &.right {
- text-align: right;
-
- .fa {
- margin: 0 10px;
- }
+ &.right .fa {
+ margin: 0 10px;
}
}
}
@@ -391,6 +394,9 @@ html, body {
/* @group Component / Footer */
+@SearchHeight: 22px;
+@SearchFontSize: 14px;
+
.footer {
margin: 0;
padding: 5px 20px;
@@ -403,6 +409,44 @@ html, body {
left: 0;
right: 0;
text-align: center;
+ line-height: @SearchHeight;
+}
+
+.github-link {
+ cursor: pointer;
+}
+
+/* @group Search input */
+
+.search-wrapper {
+ position: relative;
+ overflow: hidden;
+
+ .octicon-x {
+ position: absolute;
+ text-align: center;
+ top: 0;
+ right: 0;
+ color: @ThemeBlack;
+ cursor: pointer;
+ height: @SearchHeight;
+ width: @SearchHeight;
+ line-height: @SearchHeight;
+ }
}
+.search {
+ width: 100%;
+ border-radius: @SearchHeight / 2;
+ height: @SearchHeight;
+ padding: 0 8px;
+ color: @ThemeBlack;
+ font-size: @SearchFontSize;
+ border: none;
+ box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.5);
+ -webkit-user-select: text;
+}
+
+/* @end Search input */
+
/* @end Component / Footer */