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 */