├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── Gruntfile.js
├── LICENSE
├── README.md
├── actions
├── GroupsActions
│ ├── index.js
│ ├── src
│ │ └── GroupsActions.js
│ └── test
│ │ └── GroupsActions.js
├── SelectedGroupActions
│ ├── index.js
│ ├── src
│ │ └── SelectedGroupActions.js
│ └── test
│ │ └── SelectedGroupActions.js
├── SelectedUserActions
│ ├── index.js
│ ├── src
│ │ └── SelectedUserActions.js
│ └── test
│ │ └── SelectedUserActions.js
└── UsersActions
│ ├── index.js
│ ├── src
│ └── UsersActions.js
│ └── test
│ └── UsersActions.js
├── apps
└── Desktop
│ ├── index.html
│ ├── index.js
│ ├── index.less
│ ├── src
│ └── DesktopLauncher.jsx
│ └── test
│ └── DesktopLauncher.jsx
├── components
├── Application
│ ├── index.js
│ ├── src
│ │ ├── Application.jsx
│ │ └── Application.less
│ └── test
│ │ └── Application.jsx
├── ApplicationContent
│ ├── index.js
│ ├── src
│ │ ├── ApplicationContent.jsx
│ │ └── ApplicationContent.less
│ └── test
│ │ └── ApplicationContent.jsx
├── ApplicationHeader
│ ├── index.js
│ ├── src
│ │ ├── ApplicationHeader.jsx
│ │ └── ApplicationHeader.less
│ └── test
│ │ └── ApplicationHeader.jsx
├── Content
│ ├── index.js
│ ├── src
│ │ ├── Content.jsx
│ │ └── Content.less
│ └── test
│ │ └── Content.jsx
├── GroupItem
│ ├── index.js
│ ├── src
│ │ ├── GroupItem.jsx
│ │ └── GroupItem.less
│ └── test
│ │ └── GroupItem.jsx
├── GroupsList
│ ├── index.js
│ ├── src
│ │ └── GroupsList.jsx
│ └── test
│ │ └── GroupsList.jsx
├── GroupsPage
│ ├── index.js
│ ├── src
│ │ ├── GroupsPage.jsx
│ │ └── GroupsPage.less
│ └── test
│ │ └── GroupsPage.jsx
├── GroupsSidebar
│ ├── index.js
│ ├── src
│ │ ├── GroupsSidebar.jsx
│ │ └── GroupsSidebar.less
│ └── test
│ │ └── GroupsSidebar.jsx
├── ListHeader
│ ├── index.js
│ ├── src
│ │ ├── ListHeader.jsx
│ │ └── ListHeader.less
│ └── test
│ │ └── ListHeader.jsx
├── Navigation
│ ├── index.js
│ ├── src
│ │ ├── Navigation.jsx
│ │ └── Navigation.less
│ └── test
│ │ └── Navigation.jsx
├── NoSelection
│ ├── index.js
│ ├── src
│ │ ├── NoSelection.jsx
│ │ └── NoSelection.less
│ └── test
│ │ └── NoSelection.jsx
├── RealtimeConnection
│ ├── index.js
│ ├── src
│ │ └── RealtimeConnection.jsx
│ └── test
│ │ └── RealtimeConnection.jsx
├── SelectedGroupPage
│ ├── index.js
│ ├── src
│ │ ├── SelectedGroupPage.jsx
│ │ └── SelectedGroupPage.less
│ └── test
│ │ └── SelectedGroupPage.jsx
├── SelectedUserPage
│ ├── index.js
│ ├── src
│ │ ├── SelectedUserPage.jsx
│ │ └── SelectedUserPage.less
│ └── test
│ │ └── SelectedUserPage.jsx
├── SettingsPage
│ ├── index.js
│ ├── src
│ │ ├── SettingsPage.jsx
│ │ └── SettingsPage.less
│ └── test
│ │ └── SettingsPage.jsx
├── SkipToContent
│ ├── index.js
│ ├── src
│ │ ├── SkipToContent.jsx
│ │ └── SkipToContent.less
│ └── test
│ │ └── SkipToContent.jsx
├── UserItem
│ ├── index.js
│ ├── src
│ │ ├── UserItem.jsx
│ │ └── UserItem.less
│ └── test
│ │ └── UserItem.jsx
├── UsersList
│ ├── index.js
│ ├── src
│ │ └── UsersList.jsx
│ └── test
│ │ └── UsersList.jsx
├── UsersPage
│ ├── index.js
│ ├── src
│ │ ├── UsersPage.jsx
│ │ └── UsersPage.less
│ └── test
│ │ └── UsersPage.jsx
└── UsersSidebar
│ ├── index.js
│ ├── src
│ ├── UsersSidebar.jsx
│ └── UsersSidebar.less
│ └── test
│ └── UsersSidebar.jsx
├── constants
├── ActionTypes
│ └── index.js
└── Config
│ └── index.js
├── containers
├── ApplicationContainer
│ ├── index.js
│ ├── src
│ │ └── ApplicationContainer.jsx
│ └── test
│ │ └── ApplicationContainer.jsx
├── GroupsContainer
│ ├── index.js
│ ├── src
│ │ └── GroupsContainer.jsx
│ └── test
│ │ └── GroupsContainer.jsx
├── RootContainer
│ ├── index.js
│ ├── src
│ │ └── RootContainer.jsx
│ └── test
│ │ └── RootContainer.jsx
├── SelectedGroupContainer
│ ├── index.js
│ ├── src
│ │ └── SelectedGroupContainer.jsx
│ └── test
│ │ └── SelectedGroupContainer.jsx
├── SelectedUserContainer
│ ├── index.js
│ ├── src
│ │ └── SelectedUserContainer.jsx
│ └── test
│ │ └── SelectedUserContainer.jsx
└── UsersContainer
│ ├── index.js
│ ├── src
│ └── UsersContainer.jsx
│ └── test
│ └── UsersContainer.jsx
├── decorators
├── pureRender
│ ├── index.js
│ ├── src
│ │ └── pureRender.jsx
│ └── test
│ │ └── pureRender.jsx
├── routerHistory
│ ├── index.js
│ ├── src
│ │ └── routerHistory.jsx
│ └── test
│ │ └── routerHistory.jsx
└── runOnTransition
│ ├── index.js
│ ├── src
│ └── runOnTransition.jsx
│ └── test
│ └── runOnTransition.jsx
├── dist
├── electron
│ ├── LICENSE
│ ├── af8f3a683b533dfc69fe430ff2081bc6.ttf
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ └── stats.json
└── web
│ ├── LICENSE
│ ├── af8f3a683b533dfc69fe430ff2081bc6.ttf
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ └── stats.json
├── grunt
├── aliases.js
├── clean.js
├── concurrent.js
├── connect.js
├── karma.js
├── legal-eagle.js
├── webpack-config.js
├── webpack-dev-server.js
└── webpack.js
├── index.js
├── loaders
└── template.js
├── package.json
├── reducers
├── AllReducers
│ └── index.js
├── groupsReducer
│ ├── index.js
│ ├── src
│ │ └── groupsReducer.js
│ └── test
│ │ └── groupsReducer.js
├── selectedGroupReducer
│ ├── index.js
│ ├── src
│ │ └── selectedGroupReducer.js
│ └── test
│ │ └── selectedGroupReducer.js
├── selectedUserReducer
│ ├── index.js
│ ├── src
│ │ └── selectedUserReducer.js
│ └── test
│ │ └── selectedUserReducer.js
└── usersReducer
│ ├── index.js
│ ├── src
│ └── usersReducer.js
│ └── test
│ └── usersReducer.js
├── selectors
├── groupsSelector
│ ├── index.js
│ ├── src
│ │ └── groupsSelector.js
│ └── test
│ │ └── groupsSelector.js
├── selectedGroupSelector
│ ├── index.js
│ ├── src
│ │ └── selectedGroupSelector.js
│ └── test
│ │ └── selectedGroupSelector.js
├── selectedUserSelector
│ ├── index.js
│ ├── src
│ │ └── selectedUserSelector.js
│ └── test
│ │ └── selectedUserSelector.js
└── usersSelector
│ ├── index.js
│ ├── src
│ └── usersSelector.js
│ └── test
│ └── usersSelector.js
├── styles
├── base.less
├── config.less
└── reset.less
├── tests.js
└── utils
├── ApiHelper
├── index.js
├── src
│ └── ApiHelper.js
└── test
│ └── ApiHelper.js
├── InfoHelper
├── index.js
├── src
│ └── InfoHelper.js
└── test
│ └── InfoHelper.js
├── RealtimeHelper
├── index.js
├── src
│ └── RealtimeHelper.js
└── test
│ └── RealtimeHelper.js
└── WinControlHelper
├── index.js
├── src
└── WinControlHelper.js
└── test
└── WinControlHelper.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "optional": ["runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 |
5 | charset = utf-8
6 |
7 | end_of_line = lf
8 | insert_final_newline = true
9 |
10 | trim_trailing_whitespace = true
11 |
12 | indent_style = space
13 | indent_size = 2
14 |
15 | max_line_length = 100
16 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | dist/
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard", "standard-react"],
3 | "parser": "babel-eslint",
4 | "env": {
5 | "browser": true,
6 | "es6": true,
7 | "jasmine": true,
8 | "node": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | node_modules/
3 | *.log
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.12'
4 | - '0.10'
5 |
6 | before_install:
7 | - 'npm install -g grunt-cli'
8 |
9 | after_script:
10 | - 'find ./coverage -name lcov.info -exec cat {} \; | ./node_modules/coveralls/bin/coveralls.js'
11 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = function (grunt) {
4 | require('load-grunt-config')(grunt)
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Daniel Perez Alvarez
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React+Redux+WinJS Example [](http://travis-ci.org/unindented/react-redux-winjs-example) [](https://coveralls.io/r/unindented/react-redux-winjs-example)
2 |
3 | Example using [React](https://github.com/facebook/react) for rendering, [Redux](https://github.com/gaearon/redux) for containing state, and [WinJS](https://github.com/winjs/winjs) as UI toolkit.
4 |
5 | It also uses [Immutable](https://github.com/facebook/immutable-js) for immutable data structures, and [React Router](https://github.com/rackt/react-router) for routing.
6 |
7 |
8 | ## Meta
9 |
10 | * Code: `git clone git://github.com/unindented/react-redux-winjs-example.git`
11 | * Home:
12 |
13 |
14 | ## Contributors
15 |
16 | * Daniel Perez Alvarez ([unindented@gmail.com](mailto:unindented@gmail.com))
17 |
18 |
19 | ## License
20 |
21 | Copyright (c) 2015 Daniel Perez Alvarez ([unindented.org](https://unindented.org/)). This is free software, and may be redistributed under the terms specified in the LICENSE file.
22 |
--------------------------------------------------------------------------------
/actions/GroupsActions/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/GroupsActions'
2 |
--------------------------------------------------------------------------------
/actions/GroupsActions/src/GroupsActions.js:
--------------------------------------------------------------------------------
1 | import {ADD_GROUP, REMOVE_GROUP, UPDATE_GROUP, GET_GROUPS} from 'ActionTypes'
2 | import * as ApiHelper from 'ApiHelper'
3 |
4 | export function addGroup (group) {
5 | return {
6 | type: ADD_GROUP,
7 | payload: group
8 | }
9 | }
10 |
11 | export function removeGroup (group) {
12 | return {
13 | type: REMOVE_GROUP,
14 | payload: group
15 | }
16 | }
17 |
18 | export function updateGroup (group) {
19 | return {
20 | type: UPDATE_GROUP,
21 | payload: group
22 | }
23 | }
24 |
25 | export function getGroups () {
26 | return (dispatch, getState) => {
27 | const {groups} = getState()
28 |
29 | // Only run if we have no groups.
30 | if (groups.count()) {
31 | return
32 | }
33 |
34 | ApiHelper.getGroups()
35 | .then((groups) => dispatch({
36 | type: GET_GROUPS,
37 | payload: groups
38 | }))
39 | .catch((error) => dispatch({
40 | type: GET_GROUPS,
41 | error: true,
42 | payload: new Error(`Cannot get groups: ${error.message}`)
43 | }))
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/actions/GroupsActions/test/GroupsActions.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import {createStore, applyMiddleware, combineReducers} from 'redux'
3 | import thunk from 'redux-thunk'
4 |
5 | import groups from 'groupsReducer'
6 | import {addGroup, removeGroup, updateGroup, getGroups} from 'GroupsActions'
7 |
8 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
9 | const reducer = combineReducers({groups})
10 |
11 | const state = Immutable.fromJS({
12 | '0': {id: '0', name: 'Foo'},
13 | '1': {id: '1', name: 'Bar'}
14 | })
15 |
16 | describe('GroupsActions', () => {
17 | let store
18 |
19 | beforeEach(() => {
20 | store = createStoreWithMiddleware(reducer)
21 | })
22 |
23 | describe('.addGroup', () => {
24 | it('adds a new group', () => {
25 | const group = Immutable.fromJS({
26 | id: '2', name: 'Baz'
27 | })
28 |
29 | store.dispatch(addGroup(group))
30 | expect(store.getState().groups.get('2')).toEqualImmutable(group)
31 | })
32 | })
33 |
34 | describe('.removeGroup', () => {
35 | it('removes an existing group', () => {
36 | const group = state.get('1')
37 |
38 | store.dispatch(removeGroup(group))
39 | expect(store.getState().groups.get('1')).toBeUndefined()
40 | })
41 | })
42 |
43 | describe('.updateGroup', () => {
44 | it('updates an existing group', () => {
45 | const group = state.get('1').set('name', 'Rab')
46 |
47 | store.dispatch(updateGroup(group))
48 | expect(store.getState().groups.get('1')).toEqualImmutable(group)
49 | })
50 | })
51 |
52 | describe('.getGroups', () => {
53 | it('gets all groups', (done) => {
54 | const groups = Immutable.fromJS({
55 | '2': {id: '2', name: 'Baz'},
56 | '3': {id: '3', name: 'Qux'}
57 | })
58 |
59 | const response = {
60 | '@graph': groups.valueSeq().toJS(),
61 | '@meta': {}
62 | }
63 |
64 | spyOn(window, 'fetch').and.returnValue(
65 | Promise.resolve(new Response(JSON.stringify(response)))
66 | )
67 |
68 | store.subscribe(() => {
69 | expect(window.fetch).toHaveBeenCalled()
70 | expect(store.getState().groups).toEqualImmutable(groups)
71 | done()
72 | })
73 |
74 | store.dispatch(getGroups())
75 | })
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/actions/SelectedGroupActions/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/SelectedGroupActions'
2 |
--------------------------------------------------------------------------------
/actions/SelectedGroupActions/src/SelectedGroupActions.js:
--------------------------------------------------------------------------------
1 | import {SELECT_GROUP} from 'ActionTypes'
2 |
3 | export function selectGroup (id) {
4 | return {
5 | type: SELECT_GROUP,
6 | payload: id
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/actions/SelectedGroupActions/test/SelectedGroupActions.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware, combineReducers} from 'redux'
2 | import thunk from 'redux-thunk'
3 |
4 | import selectedGroupId from 'selectedGroupReducer'
5 | import {selectGroup} from 'SelectedGroupActions'
6 |
7 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
8 | const reducer = combineReducers({selectedGroupId})
9 |
10 | describe('SelectedGroupActions', () => {
11 | let store
12 |
13 | beforeEach(() => {
14 | store = createStoreWithMiddleware(reducer)
15 | })
16 |
17 | describe('.selectGroup', () => {
18 | it('selects the group', () => {
19 | const group = '1'
20 |
21 | store.dispatch(selectGroup(group))
22 | expect(store.getState().selectedGroupId).toBe('1')
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/actions/SelectedUserActions/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/SelectedUserActions'
2 |
--------------------------------------------------------------------------------
/actions/SelectedUserActions/src/SelectedUserActions.js:
--------------------------------------------------------------------------------
1 | import {SELECT_USER} from 'ActionTypes'
2 |
3 | export function selectUser (id) {
4 | return {
5 | type: SELECT_USER,
6 | payload: id
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/actions/SelectedUserActions/test/SelectedUserActions.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware, combineReducers} from 'redux'
2 | import thunk from 'redux-thunk'
3 |
4 | import selectedUserId from 'selectedUserReducer'
5 | import {selectUser} from 'SelectedUserActions'
6 |
7 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
8 | const reducer = combineReducers({selectedUserId})
9 |
10 | describe('SelectedUserActions', () => {
11 | let store
12 |
13 | beforeEach(() => {
14 | store = createStoreWithMiddleware(reducer)
15 | })
16 |
17 | describe('.selectUser', () => {
18 | it('selects the user', () => {
19 | const user = '1'
20 |
21 | store.dispatch(selectUser(user))
22 | expect(store.getState().selectedUserId).toBe('1')
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/actions/UsersActions/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/UsersActions'
2 |
--------------------------------------------------------------------------------
/actions/UsersActions/src/UsersActions.js:
--------------------------------------------------------------------------------
1 | import {ADD_USER, REMOVE_USER, UPDATE_USER, GET_USERS} from 'ActionTypes'
2 | import * as ApiHelper from 'ApiHelper'
3 |
4 | export function addUser (user) {
5 | return {
6 | type: ADD_USER,
7 | payload: user
8 | }
9 | }
10 |
11 | export function removeUser (user) {
12 | return {
13 | type: REMOVE_USER,
14 | payload: user
15 | }
16 | }
17 |
18 | export function updateUser (user) {
19 | return {
20 | type: UPDATE_USER,
21 | payload: user
22 | }
23 | }
24 |
25 | export function getUsers () {
26 | return (dispatch, getState) => {
27 | const {users} = getState()
28 |
29 | // Only run if we have no users.
30 | if (users.count()) {
31 | return
32 | }
33 |
34 | ApiHelper.getUsers()
35 | .then((json) => dispatch({
36 | type: GET_USERS,
37 | payload: json
38 | }))
39 | .catch((error) => dispatch({
40 | type: GET_USERS,
41 | error: true,
42 | payload: new Error(`Cannot get users: ${error.message}`)
43 | }))
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/actions/UsersActions/test/UsersActions.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import {createStore, applyMiddleware, combineReducers} from 'redux'
3 | import thunk from 'redux-thunk'
4 |
5 | import users from 'usersReducer'
6 | import {addUser, removeUser, updateUser, getUsers} from 'UsersActions'
7 |
8 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
9 | const reducer = combineReducers({users})
10 |
11 | describe('UsersActions', () => {
12 | const state = Immutable.fromJS({
13 | '0': {id: '0', firstName: 'Foo', lastName: 'Bar'},
14 | '1': {id: '1', firstName: 'Foo', lastName: 'Baz'}
15 | })
16 |
17 | let store
18 |
19 | beforeEach(() => {
20 | store = createStoreWithMiddleware(reducer)
21 | })
22 |
23 | describe('.addUser', () => {
24 | it('adds a new user', () => {
25 | const user = Immutable.fromJS({
26 | id: '2', firstName: 'Foo', lastName: 'Baz'
27 | })
28 |
29 | store.dispatch(addUser(user))
30 | expect(store.getState().users.get('2')).toEqualImmutable(user)
31 | })
32 | })
33 |
34 | describe('.removeUser', () => {
35 | it('removes an existing user', () => {
36 | const user = state.get('1')
37 |
38 | store.dispatch(removeUser(user))
39 | expect(store.getState().users.get('1')).toBeUndefined()
40 | })
41 | })
42 |
43 | describe('.updateUser', () => {
44 | it('updates an existing user', () => {
45 | const user = state.get('1').set('lastName', 'Rab')
46 |
47 | store.dispatch(updateUser(user))
48 | expect(store.getState().users.get('1')).toEqualImmutable(user)
49 | })
50 | })
51 |
52 | describe('.getUsers', () => {
53 | it('replaces all users', (done) => {
54 | const users = Immutable.fromJS({
55 | '2': {id: '2', firstName: 'Foo', lastName: 'Baz'},
56 | '3': {id: '3', firstName: 'Foo', lastName: 'Qux'}
57 | })
58 |
59 | const response = {
60 | '@graph': users.valueSeq().toJS(),
61 | '@meta': {}
62 | }
63 |
64 | spyOn(window, 'fetch').and.returnValue(
65 | Promise.resolve(new Response(JSON.stringify(response)))
66 | )
67 |
68 | store.subscribe(() => {
69 | expect(window.fetch).toHaveBeenCalled()
70 | expect(store.getState().users).toEqualImmutable(users)
71 | done()
72 | })
73 |
74 | store.dispatch(getUsers())
75 | })
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/apps/Desktop/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= package.productName %> v<%= package.version %>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/apps/Desktop/index.js:
--------------------------------------------------------------------------------
1 | import DesktopLauncher from './src/DesktopLauncher'
2 | import './index.html'
3 | import './index.less'
4 |
5 | const desktop = new DesktopLauncher()
6 | desktop.renderTo(document.body)
7 |
--------------------------------------------------------------------------------
/apps/Desktop/index.less:
--------------------------------------------------------------------------------
1 | @import "~base.less";
2 |
--------------------------------------------------------------------------------
/apps/Desktop/src/DesktopLauncher.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {createHashHistory} from 'history'
3 |
4 | import RootContainer from 'RootContainer'
5 |
6 | export default class DesktopLauncher {
7 | renderTo (target, options = {history: createHashHistory()}) {
8 | React.render(, target)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/Desktop/test/DesktopLauncher.jsx:
--------------------------------------------------------------------------------
1 | import DesktopLauncher from '../src/DesktopLauncher'
2 | import {createMemoryHistory} from 'history'
3 |
4 | const history = createMemoryHistory(['/'])
5 |
6 | describe('DesktopLauncher', () => {
7 | let instance
8 | let node
9 |
10 | beforeEach(() => {
11 | node = document.createElement('div')
12 | instance = new DesktopLauncher()
13 | instance.renderTo(node, {history})
14 | })
15 |
16 | it('is not empty', () => {
17 | expect(node).not.toBeEmpty()
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/components/Application/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/Application'
2 |
--------------------------------------------------------------------------------
/components/Application/src/Application.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 |
3 | import RealtimeConnection from 'RealtimeConnection'
4 | import ApplicationHeader from 'ApplicationHeader'
5 | import ApplicationContent from 'ApplicationContent'
6 | import Navigation from 'Navigation'
7 | import Content from 'Content'
8 | import './Application.less'
9 |
10 | export default class Application extends Component {
11 | static propTypes = {
12 | actions: PropTypes.object.isRequired,
13 | children: PropTypes.oneOfType([
14 | PropTypes.element,
15 | PropTypes.arrayOf(PropTypes.element)
16 | ])
17 | }
18 |
19 | state = {
20 | paneOpened: false
21 | }
22 |
23 | handlePaneToggle () {
24 | this.setState({paneOpened: !this.state.paneOpened})
25 | }
26 |
27 | handlePaneClosed () {
28 | this.setState({paneOpened: false})
29 | }
30 |
31 | render () {
32 | const {actions, children} = this.props
33 | const {paneOpened} = this.state
34 |
35 | const paneComponent = (
36 |
37 | )
38 |
39 | const contentComponent = (
40 |
41 | {children}
42 |
43 | )
44 |
45 | return (
46 |
47 |
49 |
50 |
53 |
54 |
59 |
60 | )
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/components/Application/src/Application.less:
--------------------------------------------------------------------------------
1 | .Application {
2 | display: flex;
3 | flex: 1;
4 | flex-flow: column;
5 | }
6 |
--------------------------------------------------------------------------------
/components/Application/test/Application.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 | import {Router, Route} from 'react-router'
3 | import {createMemoryHistory} from 'history'
4 |
5 | import Application from 'Application'
6 |
7 | const {TestUtils} = React.addons
8 | const history = createMemoryHistory(['/'])
9 |
10 | describe('Application', () => {
11 | let instance
12 |
13 | beforeEach(() => {
14 | instance = TestUtils.renderIntoDocument(
15 |
16 |
17 |
18 | )
19 | })
20 |
21 | it('has the `Application` class', () => {
22 | let node = React.findDOMNode(instance)
23 | expect(node).toHaveClass('Application')
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/components/ApplicationContent/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/ApplicationContent'
2 |
--------------------------------------------------------------------------------
/components/ApplicationContent/src/ApplicationContent.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import ReactWinJS from 'react-winjs'
3 |
4 | import './ApplicationContent.less'
5 |
6 | export default class ApplicationContent extends Component {
7 | static propTypes = {
8 | contentComponent: PropTypes.element,
9 | paneComponent: PropTypes.element,
10 | paneOpened: PropTypes.bool,
11 | onPaneClosed: PropTypes.func
12 | }
13 |
14 | static defaultProps = {
15 | paneOpened: false,
16 | onPaneClosed: () => {}
17 | }
18 |
19 | render () {
20 | const {contentComponent, paneComponent, paneOpened, onPaneClosed} = this.props
21 |
22 | return (
23 |
24 |
30 |
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components/ApplicationContent/src/ApplicationContent.less:
--------------------------------------------------------------------------------
1 | .ApplicationContent {
2 | display: flex;
3 | flex: 1 1 0%;
4 | flex-flow: column;
5 |
6 | &-split {
7 | flex: 1;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/components/ApplicationContent/test/ApplicationContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import ApplicationContent from 'ApplicationContent'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('ApplicationContent', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | )
14 | })
15 |
16 | it('has the `ApplicationContent` class', () => {
17 | let node = React.findDOMNode(instance)
18 | expect(node).toHaveClass('ApplicationContent')
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/components/ApplicationHeader/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/ApplicationHeader'
2 |
--------------------------------------------------------------------------------
/components/ApplicationHeader/src/ApplicationHeader.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import ReactWinJS from 'react-winjs'
3 |
4 | import * as InfoHelper from 'InfoHelper'
5 | import SkipToContent from 'SkipToContent'
6 | import './ApplicationHeader.less'
7 |
8 | export default class ApplicationHeader extends Component {
9 | static propTypes = {
10 | paneOpened: PropTypes.bool,
11 | onPaneToggle: PropTypes.func
12 | }
13 |
14 | static defaultProps = {
15 | paneOpened: false,
16 | onPaneToggle: () => {}
17 | }
18 |
19 | render () {
20 | const {paneOpened, onPaneToggle} = this.props
21 |
22 | return (
23 |
24 |
25 |
26 |
30 |
31 |
32 | {`${InfoHelper.getName()} v${InfoHelper.getVersion()}`}
33 |
34 |
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/components/ApplicationHeader/src/ApplicationHeader.less:
--------------------------------------------------------------------------------
1 | .ApplicationHeader {
2 | background-color: @header-background-color !important;
3 | flex: 0 0 auto;
4 |
5 | &-title {
6 | display: inline-block;
7 | margin: 0 @default-margin;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/components/ApplicationHeader/test/ApplicationHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import ApplicationHeader from 'ApplicationHeader'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('ApplicationHeader', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | )
14 | })
15 |
16 | it('has the `ApplicationHeader` class', () => {
17 | let node = React.findDOMNode(instance)
18 | expect(node).toHaveClass('ApplicationHeader')
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/components/Content/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/Content'
2 |
--------------------------------------------------------------------------------
/components/Content/src/Content.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 |
3 | import './Content.less'
4 |
5 | export default class Content extends Component {
6 | static propTypes = {
7 | children: PropTypes.oneOfType([
8 | PropTypes.element,
9 | PropTypes.arrayOf(PropTypes.element)
10 | ])
11 | }
12 |
13 | render () {
14 | const {children} = this.props
15 |
16 | return (
17 |
18 | {children}
19 |
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/components/Content/src/Content.less:
--------------------------------------------------------------------------------
1 | .Content {
2 | display: flex;
3 | position: absolute;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
--------------------------------------------------------------------------------
/components/Content/test/Content.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import Content from 'Content'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('Content', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | OHAI
14 |
15 | )
16 | })
17 |
18 | it('has the `Content` class', () => {
19 | let node = React.findDOMNode(instance)
20 | expect(node).toHaveClass('Content')
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/components/GroupItem/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/GroupItem'
2 |
--------------------------------------------------------------------------------
/components/GroupItem/src/GroupItem.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import ImmutablePropTypes from 'react-immutable-proptypes'
3 |
4 | import './GroupItem.less'
5 |
6 | export default class GroupItem extends Component {
7 | static propTypes = {
8 | group: ImmutablePropTypes.map.isRequired
9 | }
10 |
11 | render () {
12 | const {group} = this.props
13 | const name = group.get('name')
14 | const avatar = group.get('avatar')
15 |
16 | return (
17 |
18 |

21 |
22 | {name}
23 |
24 |
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/components/GroupItem/src/GroupItem.less:
--------------------------------------------------------------------------------
1 | .GroupItem {
2 | align-items: center;
3 | display: flex;
4 | flex-flow: row;
5 |
6 | &-avatar {
7 | border-radius: @list-avatar-border-radius;
8 | display: block;
9 | flex: 0 0 auto;
10 | margin: @list-avatar-margin;
11 | width: @list-avatar-width;
12 | height: @list-avatar-height;
13 | }
14 |
15 | &-name {
16 | display: block;
17 | flex: 1;
18 | margin: 0 0 0 @default-margin;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/components/GroupItem/test/GroupItem.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import GroupItem from 'GroupItem'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('GroupItem', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const group = Immutable.Map({id: '1', name: 'Foo Bar'})
13 |
14 | instance = TestUtils.renderIntoDocument(
15 |
16 | )
17 | })
18 |
19 | it('has the `GroupItem` class', () => {
20 | let node = React.findDOMNode(instance)
21 | expect(node).toHaveClass('GroupItem')
22 | })
23 |
24 | it('displays the name', () => {
25 | let node = React.findDOMNode(instance)
26 | expect(node).toHaveDescendantWithText('.GroupItem-name', 'Foo Bar')
27 | })
28 |
29 | it('displays the avatar', () => {
30 | let node = React.findDOMNode(instance)
31 | expect(node).toHaveDescendant('.GroupItem-avatar')
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/components/GroupsList/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/GroupsList'
2 |
--------------------------------------------------------------------------------
/components/GroupsList/src/GroupsList.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React, {Component, PropTypes} from 'react'
3 | import ImmutablePropTypes from 'react-immutable-proptypes'
4 | import ReactWinJS from 'react-winjs'
5 | import WinJS from 'winjs'
6 |
7 | import pureRender from 'pureRender'
8 | import routerHistory from 'routerHistory'
9 | import GroupItem from 'GroupItem'
10 | import * as WinControlHelper from 'WinControlHelper'
11 |
12 | const groupSort = (a, b) => {
13 | const an = a.name
14 | const bn = b.name
15 |
16 | if (an < bn) {
17 | return -1
18 | } else if (an > bn) {
19 | return 1
20 | } else {
21 | return 0
22 | }
23 | }
24 |
25 | const groupRenderer = ReactWinJS.reactRenderer((group) => (
26 |
27 | ))
28 |
29 | @pureRender
30 | @routerHistory
31 | export default class GroupsList extends Component {
32 | static propTypes = {
33 | groups: ImmutablePropTypes.map.isRequired,
34 | selectedGroup: ImmutablePropTypes.map
35 | }
36 |
37 | static defaultProps = {
38 | onClick: () => {}
39 | }
40 |
41 | handleSelectionChanged (evt) {
42 | const {groups, history} = this.props
43 | const listView = evt.currentTarget.winControl
44 |
45 | WinControlHelper.getFirstSelectedItem(listView).then((item) => {
46 | const group = groups.get(item.id)
47 | history.pushState(null, `/groups/${group.get('id')}`)
48 | })
49 | }
50 |
51 | handleContentAnimating (evt) {
52 | if (evt.detail.type === 'entrance') {
53 | evt.preventDefault()
54 | }
55 | }
56 |
57 | componentDidUpdate () {
58 | const group = this.props.selectedGroup
59 | const groupId = group && group.get('id')
60 | const listView = this.refs.list.winControl
61 |
62 | WinControlHelper.setSelection(listView, (item) => (item.id === groupId))
63 | }
64 |
65 | render () {
66 | const {groups} = this.props
67 | const data = new WinJS.Binding.List(groups.valueSeq().toJS())
68 | .createSorted(groupSort)
69 |
70 | return (
71 |
80 | )
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/components/GroupsList/test/GroupsList.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import GroupsList from 'GroupsList'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('GroupsList', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const groups = Immutable.fromJS({
13 | '1': {id: '1', name: 'Foo Bar'},
14 | '2': {id: '2', name: 'Baz Qux'}
15 | })
16 |
17 | instance = TestUtils.renderIntoDocument(
18 |
19 | )
20 | })
21 |
22 | it('has the `GroupsList` class', () => {
23 | let node = React.findDOMNode(instance)
24 | expect(node).toHaveClass('GroupsList')
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/components/GroupsPage/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/GroupsPage'
2 |
--------------------------------------------------------------------------------
/components/GroupsPage/src/GroupsPage.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 |
3 | import runOnTransition from 'runOnTransition'
4 | import GroupsSidebar from 'GroupsSidebar'
5 | import NoSelection from 'NoSelection'
6 | import './GroupsPage.less'
7 |
8 | @runOnTransition(
9 | (params, actions) => {
10 | actions.getGroups()
11 | }
12 | )
13 | export default class GroupsPage extends Component {
14 | static propTypes = {
15 | children: PropTypes.oneOfType([
16 | PropTypes.element,
17 | PropTypes.arrayOf(PropTypes.element)
18 | ])
19 | }
20 |
21 | render () {
22 | const {children, ...others} = this.props
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | {children || }
31 |
32 |
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/components/GroupsPage/src/GroupsPage.less:
--------------------------------------------------------------------------------
1 | .GroupsPage {
2 | display: flex;
3 | flex: 1;
4 | flex-flow: row;
5 |
6 | &-sidebar {
7 | display: flex;
8 | flex: 0 0 auto;
9 | width: @sidebar-width;
10 | }
11 |
12 | &-main {
13 | display: flex;
14 | flex: 1 1 0%;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/components/GroupsPage/test/GroupsPage.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import GroupsPage from 'GroupsPage'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('GroupsPage', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const groups = Immutable.fromJS({
13 | '1': {id: '1', name: 'Foo'},
14 | '2': {id: '2', name: 'Bar'}
15 | })
16 |
17 | const actions = {
18 | getGroups: () => {},
19 | selectGroup: () => {}
20 | }
21 |
22 | instance = TestUtils.renderIntoDocument(
23 |
24 | )
25 | })
26 |
27 | it('has the `GroupsPage` class', () => {
28 | let node = React.findDOMNode(instance)
29 | expect(node).toHaveClass('GroupsPage')
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/components/GroupsSidebar/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/GroupsSidebar'
2 |
--------------------------------------------------------------------------------
/components/GroupsSidebar/src/GroupsSidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import ImmutablePropTypes from 'react-immutable-proptypes'
3 |
4 | import GroupsList from 'GroupsList'
5 | import './GroupsSidebar.less'
6 |
7 | export default class GroupsSidebar extends Component {
8 | static propTypes = {
9 | groups: ImmutablePropTypes.map.isRequired,
10 | selectedGroup: ImmutablePropTypes.map
11 | }
12 |
13 | render () {
14 | const {groups, selectedGroup} = this.props
15 |
16 | return (
17 |
18 |
19 |
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/components/GroupsSidebar/src/GroupsSidebar.less:
--------------------------------------------------------------------------------
1 | .GroupsSidebar {
2 | flex: 1;
3 | position: relative;
4 | }
5 |
--------------------------------------------------------------------------------
/components/GroupsSidebar/test/GroupsSidebar.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import GroupsSidebar from 'GroupsSidebar'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('GroupsSidebar', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const groups = Immutable.fromJS({
13 | '1': {id: '1', name: 'Foo Bar'},
14 | '2': {id: '2', name: 'Baz Qux'}
15 | })
16 |
17 | const actions = {
18 | getGroups: () => {}
19 | }
20 |
21 | instance = TestUtils.renderIntoDocument(
22 |
23 | )
24 | })
25 |
26 | it('has the `GroupsSidebar` class', () => {
27 | let node = React.findDOMNode(instance)
28 | expect(node).toHaveClass('GroupsSidebar')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/components/ListHeader/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/ListHeader'
2 |
--------------------------------------------------------------------------------
/components/ListHeader/src/ListHeader.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 |
3 | import './ListHeader.less'
4 |
5 | export default class ListHeader extends Component {
6 | static propTypes = {
7 | header: PropTypes.string.isRequired
8 | }
9 |
10 | render () {
11 | const {header} = this.props
12 |
13 | return (
14 |
15 | {header}
16 |
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/components/ListHeader/src/ListHeader.less:
--------------------------------------------------------------------------------
1 | .ListHeader {
2 | background-color: @list-header-background-color;
3 | color: @list-header-text-color;
4 | }
5 |
--------------------------------------------------------------------------------
/components/ListHeader/test/ListHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import ListHeader from 'ListHeader'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('ListHeader', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | )
14 | })
15 |
16 | it('has the `ListHeader` class', () => {
17 | let node = React.findDOMNode(instance)
18 | expect(node).toHaveClass('ListHeader')
19 | })
20 |
21 | it('renders the header', () => {
22 | let node = React.findDOMNode(instance)
23 | expect(node).toHaveText('Foo')
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/components/Navigation/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/Navigation'
2 |
--------------------------------------------------------------------------------
/components/Navigation/src/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import ReactWinJS from 'react-winjs'
3 |
4 | import routerHistory from 'routerHistory'
5 | import './Navigation.less'
6 |
7 | const links = [
8 | {url: '/groups', label: 'Groups', icon: 'people'},
9 | {url: '/users', label: 'Users', icon: 'contact'},
10 | {url: '/settings', label: 'Settings', icon: 'settings'}
11 | ]
12 |
13 | @routerHistory
14 | export default class Navigation extends Component {
15 | static propTypes = {
16 | onClick: PropTypes.func
17 | }
18 |
19 | static defaultProps = {
20 | onClick: () => {}
21 | }
22 |
23 | handleClick (url) {
24 | this.props.onClick()
25 | this.props.history.pushState(null, url)
26 | }
27 |
28 | render () {
29 | return (
30 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/components/Navigation/src/Navigation.less:
--------------------------------------------------------------------------------
1 | .Navigation {
2 | display: flex;
3 | flex: 1;
4 | flex-flow: column;
5 |
6 | &-command {
7 | cursor: pointer;
8 | display: flex;
9 | flex: 0 0 auto;
10 | text-decoration: none;
11 | }
12 |
13 | &-command:last-child {
14 | margin-top: auto;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/components/Navigation/test/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 | import {Router, Route} from 'react-router'
3 | import {createMemoryHistory} from 'history'
4 |
5 | import Navigation from 'Navigation'
6 |
7 | const {TestUtils} = React.addons
8 | const history = createMemoryHistory(['/'])
9 |
10 | describe('Navigation', () => {
11 | let instance
12 |
13 | beforeEach(() => {
14 | instance = TestUtils.renderIntoDocument(
15 |
16 |
17 |
18 | )
19 | })
20 |
21 | it('has the `Navigation` class', () => {
22 | let node = React.findDOMNode(instance)
23 | expect(node).toHaveClass('Navigation')
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/components/NoSelection/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/NoSelection'
2 |
--------------------------------------------------------------------------------
/components/NoSelection/src/NoSelection.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 |
3 | import './NoSelection.less'
4 |
5 | export default class NoSelection extends Component {
6 | render () {
7 | return (
8 |
9 | No selection
10 |
11 | )
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/components/NoSelection/src/NoSelection.less:
--------------------------------------------------------------------------------
1 | .NoSelection {
2 | color: @subtle-color;
3 | flex: 1;
4 | margin: auto;
5 | text-align: center;
6 | }
7 |
--------------------------------------------------------------------------------
/components/NoSelection/test/NoSelection.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import NoSelection from 'NoSelection'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('NoSelection', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | )
14 | })
15 |
16 | it('has the `NoSelection` class', () => {
17 | let node = React.findDOMNode(instance)
18 | expect(node).toHaveClass('NoSelection')
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/components/RealtimeConnection/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/RealtimeConnection'
2 |
--------------------------------------------------------------------------------
/components/RealtimeConnection/src/RealtimeConnection.jsx:
--------------------------------------------------------------------------------
1 | import {Component, PropTypes} from 'react'
2 |
3 | import * as ApiHelper from 'ApiHelper'
4 | import * as RealtimeHelper from 'RealtimeHelper'
5 |
6 | export default class RealtimeConnection extends Component {
7 | static propTypes = {
8 | actions: PropTypes.object.isRequired
9 | }
10 |
11 | componentDidMount () {
12 | let connection
13 |
14 | try {
15 | connection = RealtimeHelper.createConnection()
16 | this.setState({connection})
17 | } catch (error) {
18 | console.warn(`Cannot connect to realtime: ${error.message}`)
19 | return
20 | }
21 |
22 | const {actions} = this.props
23 |
24 | connection.on('message', (evt) => {
25 | const data = JSON.parse(evt.data)
26 | if (data.create) {
27 | if (data.create.user) {
28 | ApiHelper.getUsersById(data.create.user).then((users) => {
29 | console.debug(users)
30 | })
31 | }
32 | }
33 | console.debug(data)
34 | })
35 | }
36 |
37 | componentDidUnmount () {
38 | this.state.connection.close()
39 | }
40 |
41 | shouldComponentUpdate () {
42 | return false
43 | }
44 |
45 | render () {
46 | return false
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/components/RealtimeConnection/test/RealtimeConnection.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import Content from 'Content'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('Content', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | OHAI
14 |
15 | )
16 | })
17 |
18 | it('has the `Content` class', () => {
19 | let node = React.findDOMNode(instance)
20 | expect(node).toHaveClass('Content')
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/components/SelectedGroupPage/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/SelectedGroupPage'
2 |
--------------------------------------------------------------------------------
/components/SelectedGroupPage/src/SelectedGroupPage.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import ImmutablePropTypes from 'react-immutable-proptypes'
3 |
4 | import runOnTransition from 'runOnTransition'
5 | import NoSelection from 'NoSelection'
6 |
7 | import './SelectedGroupPage.less'
8 |
9 | @runOnTransition(['id'],
10 | (params, actions) => {
11 | actions.selectGroup(params.id)
12 | },
13 | (params, actions) => {
14 | actions.selectGroup(null)
15 | }
16 | )
17 | export default class SelectedGroupPage extends Component {
18 | static propTypes = {
19 | selectedGroup: ImmutablePropTypes.map
20 | }
21 |
22 | render () {
23 | const group = this.props.selectedGroup
24 |
25 | if (!group) {
26 | return
27 | }
28 |
29 | const name = group.get('name')
30 | const avatar = group.get('avatar')
31 |
32 | return (
33 |
34 |
35 |
38 |
39 | {name}
40 |
41 |
42 |
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/components/SelectedGroupPage/src/SelectedGroupPage.less:
--------------------------------------------------------------------------------
1 | .SelectedGroupPage {
2 | flex: 1;
3 | overflow: auto;
4 |
5 | &-title {
6 | align-items: center;
7 | display: flex;
8 | flex-flow: row;
9 | padding: @page-title-padding;
10 | }
11 |
12 | &-avatar {
13 | border-radius: @page-avatar-border-radius;
14 | display: block;
15 | flex: 0 0 auto;
16 | margin: @page-avatar-margin;
17 | width: @page-avatar-width;
18 | height: @page-avatar-height;
19 | }
20 |
21 | &-name {
22 | display: block;
23 | flex: 1;
24 | margin: 0 0 0 @default-margin;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/components/SelectedGroupPage/test/SelectedGroupPage.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import SelectedGroupPage from 'SelectedGroupPage'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('SelectedGroupPage', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const group = Immutable.fromJS({
13 | id: '1', name: 'Foo Bar'
14 | })
15 |
16 | const params = {
17 | id: '1'
18 | }
19 |
20 | const actions = {
21 | selectGroup: () => {}
22 | }
23 |
24 | instance = TestUtils.renderIntoDocument(
25 |
26 | )
27 | })
28 |
29 | it('has the `SelectedGroupPage` class', () => {
30 | let node = React.findDOMNode(instance)
31 | expect(node).toHaveClass('SelectedGroupPage')
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/components/SelectedUserPage/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/SelectedUserPage'
2 |
--------------------------------------------------------------------------------
/components/SelectedUserPage/src/SelectedUserPage.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import ImmutablePropTypes from 'react-immutable-proptypes'
3 |
4 | import runOnTransition from 'runOnTransition'
5 | import NoSelection from 'NoSelection'
6 |
7 | import './SelectedUserPage.less'
8 |
9 | @runOnTransition(['id'],
10 | (params, actions) => {
11 | actions.selectUser(params.id)
12 | },
13 | (params, actions) => {
14 | actions.selectUser(null)
15 | }
16 | )
17 | export default class SelectedUserPage extends Component {
18 | static propTypes = {
19 | selectedUser: ImmutablePropTypes.map
20 | }
21 |
22 | render () {
23 | const user = this.props.selectedUser
24 |
25 | if (!user) {
26 | return
27 | }
28 |
29 | const name = `${user.get('firstName')} ${user.get('lastName')}`
30 | const avatar = user.get('avatar')
31 |
32 | return (
33 |
34 |
35 |
38 |
39 | {name}
40 |
41 |
42 |
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/components/SelectedUserPage/src/SelectedUserPage.less:
--------------------------------------------------------------------------------
1 | .SelectedUserPage {
2 | flex: 1;
3 | overflow: auto;
4 |
5 | &-title {
6 | align-items: center;
7 | display: flex;
8 | flex-flow: row;
9 | padding: @page-title-padding;
10 | }
11 |
12 | &-avatar {
13 | border-radius: @page-avatar-border-radius;
14 | display: block;
15 | flex: 0 0 auto;
16 | margin: @page-avatar-margin;
17 | width: @page-avatar-width;
18 | height: @page-avatar-height;
19 | }
20 |
21 | &-name {
22 | display: block;
23 | flex: 1;
24 | margin: 0 0 0 @default-margin;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/components/SelectedUserPage/test/SelectedUserPage.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import SelectedUserPage from 'SelectedUserPage'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('SelectedUserPage', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const user = Immutable.fromJS({
13 | id: '1', firstName: 'Foo', lastName: 'Bar'
14 | })
15 |
16 | const params = {
17 | id: '1'
18 | }
19 |
20 | const actions = {
21 | selectUser: () => {}
22 | }
23 |
24 | instance = TestUtils.renderIntoDocument(
25 |
26 | )
27 | })
28 |
29 | it('has the `SelectedUserPage` class', () => {
30 | let node = React.findDOMNode(instance)
31 | expect(node).toHaveClass('SelectedUserPage')
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/components/SettingsPage/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/SettingsPage'
2 |
--------------------------------------------------------------------------------
/components/SettingsPage/src/SettingsPage.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 |
3 | import './SettingsPage.less'
4 |
5 | export default class SettingsPage extends Component {
6 | render () {
7 | return (
8 |
9 |
10 | Settings
11 |
12 |
13 | )
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/components/SettingsPage/src/SettingsPage.less:
--------------------------------------------------------------------------------
1 | .SettingsPage {
2 | flex: 1;
3 | overflow: auto;
4 |
5 | &-title {
6 | padding: @page-title-padding;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/components/SettingsPage/test/SettingsPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import SettingsPage from 'SettingsPage'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('SettingsPage', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | )
14 | })
15 |
16 | it('has the `SettingsPage` class', () => {
17 | let node = React.findDOMNode(instance)
18 | expect(node).toHaveClass('SettingsPage')
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/components/SkipToContent/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/SkipToContent'
2 |
--------------------------------------------------------------------------------
/components/SkipToContent/src/SkipToContent.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 |
3 | import './SkipToContent.less'
4 |
5 | export default class Content extends Component {
6 | handleClick (evt) {
7 | evt.preventDefault()
8 | // Focus on the first element with a non-negative `tabindex` value.
9 | document.querySelector('#main [tabindex]:not([tabindex^="-"])').focus()
10 | }
11 |
12 | render () {
13 | return (
14 |
16 | Skip to main content
17 |
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/components/SkipToContent/src/SkipToContent.less:
--------------------------------------------------------------------------------
1 | .SkipToContent {
2 | display: block;
3 | line-height: @header-height;
4 | padding: 0 @default-padding;
5 | text-decoration: none;
6 | transition: top .25s ease-in-out;
7 | position: absolute;
8 | top: -@header-height;
9 | left: 0;
10 |
11 | &:active,
12 | &:focus,
13 | &:hover {
14 | top: 0;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/components/SkipToContent/test/SkipToContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 |
3 | import Content from 'Content'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('Content', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | instance = TestUtils.renderIntoDocument(
12 |
13 | OHAI
14 |
15 | )
16 | })
17 |
18 | it('has the `Content` class', () => {
19 | let node = React.findDOMNode(instance)
20 | expect(node).toHaveClass('Content')
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/components/UserItem/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/UserItem'
2 |
--------------------------------------------------------------------------------
/components/UserItem/src/UserItem.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import ImmutablePropTypes from 'react-immutable-proptypes'
3 |
4 | import './UserItem.less'
5 |
6 | export default class UserItem extends Component {
7 | static propTypes = {
8 | user: ImmutablePropTypes.map.isRequired
9 | }
10 |
11 | render () {
12 | const {user} = this.props
13 | const fullName = `${user.get('firstName')} ${user.get('lastName')}`
14 | const jobTitle = user.get('jobTitle')
15 | const avatar = user.get('avatar')
16 |
17 | return (
18 |
19 |

22 |
23 |
24 | {fullName}
25 |
26 |
27 | {jobTitle}
28 |
29 |
30 |
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components/UserItem/src/UserItem.less:
--------------------------------------------------------------------------------
1 | .UserItem {
2 | align-items: center;
3 | display: flex;
4 | flex-flow: row;
5 |
6 | &-avatar {
7 | border-radius: @list-avatar-border-radius;
8 | display: block;
9 | flex: 0 0 auto;
10 | margin: @list-avatar-margin;
11 | width: @list-avatar-width;
12 | height: @list-avatar-height;
13 | }
14 |
15 | &-info {
16 | display: flex;
17 | flex: 1;
18 | flex-flow: column;
19 | margin: 0 0 0 @default-margin;
20 | }
21 |
22 | &-name,
23 | &-misc {
24 | display: block;
25 | flex: 1;
26 | }
27 |
28 | &-misc {
29 | color: @subtle-color;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/components/UserItem/test/UserItem.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import UserItem from 'UserItem'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('UserItem', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const user = Immutable.Map({
13 | id: '1',
14 | firstName: 'Foo',
15 | lastName: 'Bar',
16 | jobTitle: 'Baz',
17 | avatar: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
18 | })
19 |
20 | instance = TestUtils.renderIntoDocument(
21 |
22 | )
23 | })
24 |
25 | it('has the `UserItem` class', () => {
26 | let node = React.findDOMNode(instance)
27 | expect(node).toHaveClass('UserItem')
28 | })
29 |
30 | it('displays the first and last names', () => {
31 | let node = React.findDOMNode(instance)
32 | expect(node).toHaveDescendantWithText('.UserItem-name', 'Foo Bar')
33 | })
34 |
35 | it('displays the job title', () => {
36 | let node = React.findDOMNode(instance)
37 | expect(node).toHaveDescendantWithText('.UserItem-misc', 'Baz')
38 | })
39 |
40 | it('displays the avatar', () => {
41 | let node = React.findDOMNode(instance)
42 | expect(node).toHaveDescendant('.UserItem-avatar')
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/components/UsersList/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/UsersList'
2 |
--------------------------------------------------------------------------------
/components/UsersList/src/UsersList.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React, {Component, PropTypes} from 'react'
3 | import ImmutablePropTypes from 'react-immutable-proptypes'
4 | import ReactWinJS from 'react-winjs'
5 | import WinJS from 'winjs'
6 |
7 | import pureRender from 'pureRender'
8 | import routerHistory from 'routerHistory'
9 | import ListHeader from 'ListHeader'
10 | import UserItem from 'UserItem'
11 | import * as WinControlHelper from 'WinControlHelper'
12 |
13 | const groupKey = (data) => (
14 | data.firstName.charAt(0).toUpperCase()
15 | )
16 |
17 | const groupData = (data) => (
18 | {title: groupKey(data)}
19 | )
20 |
21 | const userSort = (a, b) => {
22 | const an = a.firstName
23 | const bn = b.firstName
24 |
25 | if (an < bn) {
26 | return -1
27 | } else if (an > bn) {
28 | return 1
29 | } else {
30 | return 0
31 | }
32 | }
33 |
34 | const headerRenderer = ReactWinJS.reactRenderer((header) => (
35 |
36 | ))
37 |
38 | const userRenderer = ReactWinJS.reactRenderer((user) => (
39 |
40 | ))
41 |
42 | @pureRender
43 | @routerHistory
44 | export default class UsersList extends Component {
45 | static propTypes = {
46 | users: ImmutablePropTypes.map.isRequired,
47 | selectedUser: ImmutablePropTypes.map
48 | }
49 |
50 | handleSelectionChanged (evt) {
51 | const {users, history} = this.props
52 | const listView = evt.currentTarget.winControl
53 |
54 | WinControlHelper.getFirstSelectedItem(listView).then((item) => {
55 | const user = users.get(item.id)
56 | history.pushState(null, `/users/${user.get('id')}`)
57 | })
58 | }
59 |
60 | handleContentAnimating (evt) {
61 | if (evt.detail.type === 'entrance') {
62 | evt.preventDefault()
63 | }
64 | }
65 |
66 | componentDidUpdate () {
67 | const user = this.props.selectedUser
68 | const userId = user && user.get('id')
69 | const listView = this.refs.list.winControl
70 |
71 | WinControlHelper.setSelection(listView, (item) => (item.id === userId))
72 | }
73 |
74 | render () {
75 | const {users} = this.props
76 | const data = new WinJS.Binding.List(users.valueSeq().toJS())
77 | .createSorted(userSort)
78 | .createGrouped(groupKey, groupData)
79 |
80 | return (
81 |
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/components/UsersList/test/UsersList.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import UsersList from 'UsersList'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('UsersList', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const users = Immutable.fromJS({
13 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
14 | '2': {id: '2', firstName: 'Baz', lastName: 'Qux'}
15 | })
16 |
17 | instance = TestUtils.renderIntoDocument(
18 |
19 | )
20 | })
21 |
22 | it('has the `UsersList` class', () => {
23 | let node = React.findDOMNode(instance)
24 | expect(node).toHaveClass('UsersList')
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/components/UsersPage/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/UsersPage'
2 |
--------------------------------------------------------------------------------
/components/UsersPage/src/UsersPage.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 |
3 | import runOnTransition from 'runOnTransition'
4 | import UsersSidebar from 'UsersSidebar'
5 | import NoSelection from 'NoSelection'
6 | import './UsersPage.less'
7 |
8 | @runOnTransition(
9 | (params, actions) => {
10 | actions.getUsers()
11 | }
12 | )
13 | export default class UsersPage extends Component {
14 | static propTypes = {
15 | children: PropTypes.oneOfType([
16 | PropTypes.element,
17 | PropTypes.arrayOf(PropTypes.element)
18 | ])
19 | }
20 |
21 | render () {
22 | const {children, ...others} = this.props
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | {children || }
31 |
32 |
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/components/UsersPage/src/UsersPage.less:
--------------------------------------------------------------------------------
1 | .UsersPage {
2 | display: flex;
3 | flex: 1;
4 | flex-flow: row;
5 |
6 | &-sidebar {
7 | display: flex;
8 | flex: 0 0 auto;
9 | width: @sidebar-width;
10 | }
11 |
12 | &-main {
13 | display: flex;
14 | flex: 1 1 0%;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/components/UsersPage/test/UsersPage.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import UsersPage from 'UsersPage'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('UsersPage', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const users = Immutable.fromJS({
13 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
14 | '2': {id: '2', firstName: 'Foo', lastName: 'Baz'}
15 | })
16 |
17 | const actions = {
18 | getUsers: () => {},
19 | selectUser: () => {}
20 | }
21 |
22 | instance = TestUtils.renderIntoDocument(
23 |
24 | )
25 | })
26 |
27 | it('has the `UsersPage` class', () => {
28 | let node = React.findDOMNode(instance)
29 | expect(node).toHaveClass('UsersPage')
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/components/UsersSidebar/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/UsersSidebar'
2 |
--------------------------------------------------------------------------------
/components/UsersSidebar/src/UsersSidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import ImmutablePropTypes from 'react-immutable-proptypes'
3 |
4 | import UsersList from 'UsersList'
5 | import './UsersSidebar.less'
6 |
7 | export default class UsersSidebar extends Component {
8 | static propTypes = {
9 | users: ImmutablePropTypes.map.isRequired,
10 | selectedUser: ImmutablePropTypes.map,
11 | }
12 |
13 | render () {
14 | const {users, selectedUser} = this.props
15 |
16 | return (
17 |
18 |
19 |
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/components/UsersSidebar/src/UsersSidebar.less:
--------------------------------------------------------------------------------
1 | .UsersSidebar {
2 | flex: 1;
3 | position: relative;
4 | }
5 |
--------------------------------------------------------------------------------
/components/UsersSidebar/test/UsersSidebar.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 |
4 | import UsersSidebar from 'UsersSidebar'
5 |
6 | const {TestUtils} = React.addons
7 |
8 | describe('UsersSidebar', () => {
9 | let instance
10 |
11 | beforeEach(() => {
12 | const users = Immutable.fromJS({
13 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
14 | '2': {id: '2', firstName: 'Baz', lastName: 'Qux'}
15 | })
16 |
17 | const actions = {
18 | getUsers: () => {}
19 | }
20 |
21 | instance = TestUtils.renderIntoDocument(
22 |
23 | )
24 | })
25 |
26 | it('has the `UsersSidebar` class', () => {
27 | let node = React.findDOMNode(instance)
28 | expect(node).toHaveClass('UsersSidebar')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/constants/ActionTypes/index.js:
--------------------------------------------------------------------------------
1 | export const ADD_GROUP = 'ADD_GROUP'
2 | export const REMOVE_GROUP = 'REMOVE_GROUP'
3 | export const UPDATE_GROUP = 'UPDATE_GROUP'
4 |
5 | export const ADD_USER = 'ADD_USER'
6 | export const REMOVE_USER = 'REMOVE_USER'
7 | export const UPDATE_USER = 'UPDATE_USER'
8 |
9 | export const GET_GROUPS = 'GET_GROUPS'
10 | export const SELECT_GROUP = 'SELECT_GROUP'
11 |
12 | export const GET_USERS = 'GET_USERS'
13 | export const SELECT_USER = 'SELECT_USER'
14 |
--------------------------------------------------------------------------------
/constants/Config/index.js:
--------------------------------------------------------------------------------
1 | export const FRONT_URL = (process.env.NODE_ENV === 'production' ?
2 | 'https://unindented.github.io/react-redux-winjs-example/' :
3 | 'http://localhost:8000')
4 | export const BACK_URL = (process.env.NODE_ENV === 'production' ?
5 | 'https://fort.herokuapp.com' :
6 | 'http://localhost:8001')
7 |
8 | export const REALTIME_URL = `${BACK_URL.replace(/https?/, 'ws')}/realtime`
9 |
10 | export const API_URL = `${BACK_URL}/api`
11 | export const GROUPS_API_URL = `${API_URL}/groups`
12 | export const GROUPS_BY_ID_API_URL = `${GROUPS_API_URL}/\${ids.join(',')}`
13 | export const USERS_API_URL = `${API_URL}/users`
14 | export const USERS_BY_ID_API_URL = `${USERS_API_URL}/\${ids.join(',')}`
15 |
16 | export const ARTIFICIAL_DELAY = (process.env.NODE_ENV === 'test' ||
17 | process.env.NODE_ENV === 'production' ?
18 | 0 :
19 | 1000)
20 |
--------------------------------------------------------------------------------
/containers/ApplicationContainer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/ApplicationContainer'
2 |
--------------------------------------------------------------------------------
/containers/ApplicationContainer/src/ApplicationContainer.jsx:
--------------------------------------------------------------------------------
1 | import assign from 'lodash/object/assign'
2 | import React, {Component, PropTypes} from 'react'
3 | import {bindActionCreators} from 'redux'
4 | import {connect} from 'react-redux'
5 |
6 | import Application from 'Application'
7 | import * as GroupsActions from 'GroupsActions'
8 | import * as UsersActions from 'UsersActions'
9 |
10 | @connect((state) => state)
11 | export default class ApplicationContainer extends Component {
12 | static propTypes = {
13 | dispatch: PropTypes.func.isRequired,
14 | children: PropTypes.oneOfType([
15 | PropTypes.element,
16 | PropTypes.arrayOf(PropTypes.element)
17 | ])
18 | }
19 |
20 | render () {
21 | const {dispatch, children, ...others} = this.props
22 | const actions = assign({}, GroupsActions, UsersActions)
23 |
24 | return (
25 |
26 | {children}
27 |
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/containers/ApplicationContainer/test/ApplicationContainer.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 | import {createStore, applyMiddleware, combineReducers} from 'redux'
4 | import thunk from 'redux-thunk'
5 | import {Provider} from 'react-redux'
6 |
7 | import ApplicationContainer from 'ApplicationContainer'
8 | import * as reducers from 'AllReducers'
9 |
10 | const {TestUtils} = React.addons
11 |
12 | const groups = Immutable.fromJS({
13 | '1': {id: '1', name: 'Foo Bar'},
14 | '2': {id: '2', name: 'Baz Qux'}
15 | })
16 |
17 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
18 | const reducer = combineReducers(reducers)
19 | const store = createStoreWithMiddleware(reducer, {groups})
20 |
21 | describe('ApplicationContainer', () => {
22 | let instance
23 |
24 | beforeEach(() => {
25 | const data = {
26 | '@graph': groups.valueSeq().toJS(),
27 | '@meta': {}
28 | }
29 |
30 | spyOn(window, 'fetch').and.returnValue(
31 | Promise.resolve(new Response(JSON.stringify(data)))
32 | )
33 |
34 | instance = TestUtils.renderIntoDocument(
35 |
36 | {() => }
37 |
38 | )
39 | })
40 |
41 | it('is not empty', () => {
42 | let node = React.findDOMNode(instance)
43 | expect(node).not.toBeEmpty()
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/containers/GroupsContainer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/GroupsContainer'
2 |
--------------------------------------------------------------------------------
/containers/GroupsContainer/src/GroupsContainer.jsx:
--------------------------------------------------------------------------------
1 | import assign from 'lodash/object/assign'
2 | import React, {Component, PropTypes} from 'react'
3 | import {bindActionCreators} from 'redux'
4 | import {connect} from 'react-redux'
5 |
6 | import groupsSelector from 'groupsSelector'
7 | import selectedGroupSelector from 'selectedGroupSelector'
8 | import GroupsPage from 'GroupsPage'
9 | import NoSelection from 'NoSelection'
10 | import * as GroupsActions from 'GroupsActions'
11 | import * as SelectedGroupActions from 'SelectedGroupActions'
12 |
13 | @connect((state) => ({
14 | groups: groupsSelector(state),
15 | selectedGroup: selectedGroupSelector(state)
16 | }))
17 | export default class GroupsContainer extends Component {
18 | static propTypes = {
19 | dispatch: PropTypes.func.isRequired,
20 | children: PropTypes.oneOfType([
21 | PropTypes.element,
22 | PropTypes.arrayOf(PropTypes.element)
23 | ])
24 | }
25 |
26 | render () {
27 | const {dispatch, children, ...others} = this.props
28 | const actions = assign({}, GroupsActions, SelectedGroupActions)
29 |
30 | return (
31 |
32 | {children}
33 |
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/containers/GroupsContainer/test/GroupsContainer.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 | import {createStore, applyMiddleware, combineReducers} from 'redux'
4 | import thunk from 'redux-thunk'
5 | import {Provider} from 'react-redux'
6 |
7 | import GroupsContainer from 'GroupsContainer'
8 | import * as reducers from 'AllReducers'
9 |
10 | const {TestUtils} = React.addons
11 |
12 | const groups = Immutable.fromJS({
13 | '1': {id: '1', name: 'Foo Bar'},
14 | '2': {id: '2', name: 'Baz Qux'}
15 | })
16 |
17 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
18 | const reducer = combineReducers(reducers)
19 | const store = createStoreWithMiddleware(reducer, {groups})
20 |
21 | describe('GroupsContainer', () => {
22 | let instance
23 |
24 | beforeEach(() => {
25 | const data = {
26 | '@graph': groups.valueSeq().toJS(),
27 | '@meta': {}
28 | }
29 |
30 | spyOn(window, 'fetch').and.returnValue(
31 | Promise.resolve(new Response(JSON.stringify(data)))
32 | )
33 |
34 | instance = TestUtils.renderIntoDocument(
35 |
36 | {() => }
37 |
38 | )
39 | })
40 |
41 | it('is not empty', () => {
42 | let node = React.findDOMNode(instance)
43 | expect(node).not.toBeEmpty()
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/containers/RootContainer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/RootContainer'
2 |
--------------------------------------------------------------------------------
/containers/RootContainer/src/RootContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import {Router, Route} from 'react-router'
3 | import {createStore, applyMiddleware, combineReducers} from 'redux'
4 | import thunk from 'redux-thunk'
5 | import {Provider} from 'react-redux'
6 |
7 | import ApplicationContainer from 'ApplicationContainer'
8 | import GroupsContainer from 'GroupsContainer'
9 | import SelectedGroupContainer from 'SelectedGroupContainer'
10 | import UsersContainer from 'UsersContainer'
11 | import SelectedUserContainer from 'SelectedUserContainer'
12 | import SettingsPage from 'SettingsPage'
13 | import * as reducers from 'AllReducers'
14 |
15 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
16 | const reducer = combineReducers(reducers)
17 | const store = createStoreWithMiddleware(reducer)
18 |
19 | const renderRoutes = function (history) {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | )
33 | }
34 |
35 | export default class RootContainer extends Component {
36 | static propTypes = {
37 | history: PropTypes.object.isRequired
38 | }
39 |
40 | render () {
41 | const {history} = this.props
42 |
43 | return (
44 |
45 | {renderRoutes.bind(null, history)}
46 |
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/containers/RootContainer/test/RootContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons'
2 | import {createMemoryHistory} from 'history'
3 |
4 | import RootContainer from 'RootContainer'
5 |
6 | const {TestUtils} = React.addons
7 | const history = createMemoryHistory(['/'])
8 |
9 | describe('RootContainer', () => {
10 | let instance
11 |
12 | beforeEach(() => {
13 | instance = TestUtils.renderIntoDocument(
14 |
15 | )
16 | })
17 |
18 | it('is not empty', () => {
19 | let node = React.findDOMNode(instance)
20 | expect(node).not.toBeEmpty()
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/containers/SelectedGroupContainer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/SelectedGroupContainer'
2 |
--------------------------------------------------------------------------------
/containers/SelectedGroupContainer/src/SelectedGroupContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import {bindActionCreators} from 'redux'
3 | import {connect} from 'react-redux'
4 |
5 | import selectedGroupSelector from 'selectedGroupSelector'
6 | import SelectedGroupPage from 'SelectedGroupPage'
7 | import * as SelectedGroupActions from 'SelectedGroupActions'
8 |
9 | @connect((state) => ({
10 | selectedGroup: selectedGroupSelector(state)
11 | }))
12 | export default class SelectedGroupContainer extends Component {
13 | static propTypes = {
14 | dispatch: PropTypes.func.isRequired
15 | }
16 |
17 | render () {
18 | const {dispatch, ...others} = this.props
19 |
20 | return (
21 |
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/containers/SelectedGroupContainer/test/SelectedGroupContainer.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 | import {createStore, applyMiddleware, combineReducers} from 'redux'
4 | import thunk from 'redux-thunk'
5 | import {Provider} from 'react-redux'
6 |
7 | import SelectedGroupContainer from 'SelectedGroupContainer'
8 | import * as reducers from 'AllReducers'
9 |
10 | const {TestUtils} = React.addons
11 |
12 | const group = Immutable.fromJS({
13 | id: '1', name: 'Foo Bar'
14 | })
15 |
16 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
17 | const reducer = combineReducers(reducers)
18 | const store = createStoreWithMiddleware(reducer, {group})
19 |
20 | describe('SelectedGroupContainer', () => {
21 | let instance
22 |
23 | beforeEach(() => {
24 | const data = {
25 | '@graph': [group.toJS()],
26 | '@meta': {}
27 | }
28 |
29 | spyOn(window, 'fetch').and.returnValue(
30 | Promise.resolve(new Response(JSON.stringify(data)))
31 | )
32 |
33 | instance = TestUtils.renderIntoDocument(
34 |
35 | {() => }
36 |
37 | )
38 | })
39 |
40 | it('is not empty', () => {
41 | let node = React.findDOMNode(instance)
42 | expect(node).not.toBeEmpty()
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/containers/SelectedUserContainer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/SelectedUserContainer'
2 |
--------------------------------------------------------------------------------
/containers/SelectedUserContainer/src/SelectedUserContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import {bindActionCreators} from 'redux'
3 | import {connect} from 'react-redux'
4 |
5 | import selectedUserSelector from 'selectedUserSelector'
6 | import SelectedUserPage from 'SelectedUserPage'
7 | import * as SelectedUserActions from 'SelectedUserActions'
8 |
9 | @connect((state) => ({
10 | selectedUser: selectedUserSelector(state)
11 | }))
12 | export default class SelectedUserContainer extends Component {
13 | static propTypes = {
14 | dispatch: PropTypes.func.isRequired
15 | }
16 |
17 | render () {
18 | const {dispatch, ...others} = this.props
19 |
20 | return (
21 |
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/containers/SelectedUserContainer/test/SelectedUserContainer.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 | import {createStore, applyMiddleware, combineReducers} from 'redux'
4 | import thunk from 'redux-thunk'
5 | import {Provider} from 'react-redux'
6 |
7 | import SelectedUserContainer from 'SelectedUserContainer'
8 | import * as reducers from 'AllReducers'
9 |
10 | const {TestUtils} = React.addons
11 |
12 | const user = Immutable.fromJS({
13 | id: '1', firstName: 'Foo', lastName: 'Bar'
14 | })
15 |
16 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
17 | const reducer = combineReducers(reducers)
18 | const store = createStoreWithMiddleware(reducer, {user})
19 |
20 | describe('SelectedUserContainer', () => {
21 | let instance
22 |
23 | beforeEach(() => {
24 | const data = {
25 | '@graph': [user.toJS()],
26 | '@meta': {}
27 | }
28 |
29 | spyOn(window, 'fetch').and.returnValue(
30 | Promise.resolve(new Response(JSON.stringify(data)))
31 | )
32 |
33 | instance = TestUtils.renderIntoDocument(
34 |
35 | {() => }
36 |
37 | )
38 | })
39 |
40 | it('is not empty', () => {
41 | let node = React.findDOMNode(instance)
42 | expect(node).not.toBeEmpty()
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/containers/UsersContainer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/UsersContainer'
2 |
--------------------------------------------------------------------------------
/containers/UsersContainer/src/UsersContainer.jsx:
--------------------------------------------------------------------------------
1 | import assign from 'lodash/object/assign'
2 | import React, {Component, PropTypes} from 'react'
3 | import {bindActionCreators} from 'redux'
4 | import {connect} from 'react-redux'
5 |
6 | import usersSelector from 'usersSelector'
7 | import selectedUserSelector from 'selectedUserSelector'
8 | import UsersPage from 'UsersPage'
9 | import * as UsersActions from 'UsersActions'
10 | import * as SelectedUserActions from 'SelectedUserActions'
11 |
12 | @connect((state) => ({
13 | users: usersSelector(state),
14 | selectedUser: selectedUserSelector(state)
15 | }))
16 | export default class UsersContainer extends Component {
17 | static propTypes = {
18 | dispatch: PropTypes.func.isRequired,
19 | children: PropTypes.oneOfType([
20 | PropTypes.element,
21 | PropTypes.arrayOf(PropTypes.element)
22 | ])
23 | }
24 |
25 | render () {
26 | const {dispatch, children, ...others} = this.props
27 | const actions = assign({}, UsersActions, SelectedUserActions)
28 |
29 | return (
30 |
31 | {children}
32 |
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/containers/UsersContainer/test/UsersContainer.jsx:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React from 'react/addons'
3 | import {createStore, applyMiddleware, combineReducers} from 'redux'
4 | import thunk from 'redux-thunk'
5 | import {Provider} from 'react-redux'
6 |
7 | import UsersContainer from 'UsersContainer'
8 | import * as reducers from 'AllReducers'
9 |
10 | const {TestUtils} = React.addons
11 |
12 | const users = Immutable.fromJS({
13 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
14 | '2': {id: '2', firstName: 'Baz', lastName: 'Qux'}
15 | })
16 |
17 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
18 | const reducer = combineReducers(reducers)
19 | const store = createStoreWithMiddleware(reducer, {users})
20 |
21 | describe('UsersContainer', () => {
22 | let instance
23 |
24 | beforeEach(() => {
25 | const data = {
26 | '@graph': users.valueSeq().toJS(),
27 | '@meta': {}
28 | }
29 |
30 | spyOn(window, 'fetch').and.returnValue(
31 | Promise.resolve(new Response(JSON.stringify(data)))
32 | )
33 |
34 | instance = TestUtils.renderIntoDocument(
35 |
36 | {() => }
37 |
38 | )
39 | })
40 |
41 | it('is not empty', () => {
42 | let node = React.findDOMNode(instance)
43 | expect(node).not.toBeEmpty()
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/decorators/pureRender/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/pureRender'
2 |
--------------------------------------------------------------------------------
/decorators/pureRender/src/pureRender.jsx:
--------------------------------------------------------------------------------
1 | import shallowEqual from 'shallowequal'
2 |
3 | const shouldComponentUpdate = function (nextProps, nextState) {
4 | return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)
5 | }
6 |
7 | export default (DecoratedComponent) => {
8 | if (DecoratedComponent.prototype.shouldComponentUpdate) {
9 | throw new Error('Called on a component that already implements `shouldComponentUpdate`')
10 | }
11 |
12 | DecoratedComponent.prototype.shouldComponentUpdate = shouldComponentUpdate
13 | return DecoratedComponent
14 | }
15 |
--------------------------------------------------------------------------------
/decorators/pureRender/test/pureRender.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react/addons'
2 |
3 | import pureRender from 'pureRender'
4 |
5 | const {TestUtils} = React.addons
6 |
7 | describe('pureRender', () => {
8 | let instance
9 |
10 | beforeEach(() => {
11 | @pureRender
12 | class Parent extends Component {
13 | state = {
14 | point: {x: 0}
15 | }
16 |
17 | render () {
18 | return (
19 |
20 | )
21 | }
22 | }
23 |
24 | class Child extends Component {
25 | state = {
26 | point: {y: 0}
27 | }
28 |
29 | render () {
30 | return (
31 | {this.props.x}:{this.state.point.y}
32 | )
33 | }
34 | }
35 |
36 | instance = TestUtils.renderIntoDocument(
37 |
38 | )
39 |
40 | spyOn(instance, 'render').and.callThrough()
41 | })
42 |
43 | it('renders correctly', () => {
44 | instance.setState({point: {x: 0}})
45 |
46 | let node = React.findDOMNode(instance)
47 | expect(node).toHaveText('0:0')
48 | })
49 |
50 | it('re-renders when the new values are different to the old ones', () => {
51 | instance.setState({point: {x: 0}})
52 | expect(instance.render).toHaveBeenCalled()
53 | })
54 |
55 | it('does not re-render when the new values are equal to the old ones', () => {
56 | instance.setState({point: instance.state.point})
57 | expect(instance.render).not.toHaveBeenCalled()
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/decorators/routerHistory/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/routerHistory'
2 |
--------------------------------------------------------------------------------
/decorators/routerHistory/src/routerHistory.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {History} from 'react-router'
3 |
4 | export default function (DecoratedComponent) {
5 | return React.createClass({
6 | mixins: [History],
7 |
8 | render () {
9 | return (
10 |
11 | )
12 | }
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/decorators/routerHistory/test/routerHistory.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react/addons'
2 | import {Router, Route} from 'react-router'
3 | import {createMemoryHistory} from 'history'
4 |
5 | import routerHistory from 'routerHistory'
6 |
7 | const {TestUtils} = React.addons
8 | const history = createMemoryHistory(['/groups/1'])
9 |
10 | describe('routerHistory', () => {
11 | let instance
12 | let enterSpy
13 | let leaveSpy
14 |
15 | beforeEach(() => {
16 | enterSpy = jasmine.createSpy()
17 | leaveSpy = jasmine.createSpy()
18 |
19 | @routerHistory
20 | class Test extends Component {
21 | render () {
22 | return (
23 | {this.props.params.id}
24 | )
25 | }
26 | }
27 |
28 | instance = TestUtils.renderIntoDocument(
29 |
30 |
31 |
32 | )
33 | })
34 |
35 | it('renders correctly', () => {
36 | let node = React.findDOMNode(instance)
37 | expect(node).toHaveText('1')
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/decorators/runOnTransition/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/runOnTransition'
2 |
--------------------------------------------------------------------------------
/decorators/runOnTransition/src/runOnTransition.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import pick from 'lodash/object/pick'
3 | import shallowEqual from 'shallowequal'
4 |
5 | export default function (paramKeys, enter, leave) {
6 | if (typeof paramKeys === 'function') {
7 | leave = enter
8 | enter = paramKeys
9 | paramKeys = []
10 | }
11 |
12 | return (DecoratedComponent) => (
13 | class RunOnTransitionDecorator extends Component {
14 | static propTypes = {
15 | actions: PropTypes.object.isRequired
16 | }
17 |
18 | componentWillMount () {
19 | if (enter) {
20 | enter(pick(this.props.params, paramKeys), this.props.actions)
21 | }
22 | }
23 |
24 | componentWillUnmount () {
25 | if (leave) {
26 | leave(pick(this.props.params, paramKeys), this.props.actions)
27 | }
28 | }
29 |
30 | componentDidUpdate (prevProps) {
31 | const params = pick(this.props.params, paramKeys)
32 | const prevParams = pick(prevProps.params, paramKeys)
33 |
34 | if (enter && !shallowEqual(params, prevParams)) {
35 | enter(params, this.props.actions)
36 | }
37 | }
38 |
39 | render () {
40 | return (
41 |
42 | )
43 | }
44 | }
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/decorators/runOnTransition/test/runOnTransition.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react/addons'
2 | import {Router, Route} from 'react-router'
3 | import {createMemoryHistory} from 'history'
4 |
5 | import runOnTransition from 'runOnTransition'
6 |
7 | const {TestUtils} = React.addons
8 | const history = createMemoryHistory(['/groups/1'])
9 |
10 | describe('runOnTransition', () => {
11 | let instance
12 | let enterSpy
13 | let leaveSpy
14 |
15 | beforeEach(() => {
16 | enterSpy = jasmine.createSpy()
17 | leaveSpy = jasmine.createSpy()
18 |
19 | @runOnTransition(['id'],
20 | (params, actions) => {
21 | const {id} = params
22 | enterSpy(id)
23 | },
24 | leaveSpy
25 | )
26 | class Test extends Component {
27 | render () {
28 | return (
29 | {this.props.params.id}
30 | )
31 | }
32 | }
33 |
34 | instance = TestUtils.renderIntoDocument(
35 |
36 |
37 |
38 | )
39 | })
40 |
41 | it('renders correctly', () => {
42 | let node = React.findDOMNode(instance)
43 | expect(node).toHaveText('1')
44 | })
45 |
46 | it('runs the `enter` action', () => {
47 | expect(enterSpy).toHaveBeenCalledWith('1')
48 | })
49 |
50 | it('does not run the `leave` action', () => {
51 | expect(leaveSpy).not.toHaveBeenCalled()
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/dist/electron/af8f3a683b533dfc69fe430ff2081bc6.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unindented/react-redux-winjs-example/01173a0c79e79655d270d9c0fb2f4a8028249cff/dist/electron/af8f3a683b533dfc69fe430ff2081bc6.ttf
--------------------------------------------------------------------------------
/dist/electron/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React+Redux+WinJS v0.0.0
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/dist/web/af8f3a683b533dfc69fe430ff2081bc6.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unindented/react-redux-winjs-example/01173a0c79e79655d270d9c0fb2f4a8028249cff/dist/web/af8f3a683b533dfc69fe430ff2081bc6.ttf
--------------------------------------------------------------------------------
/dist/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React+Redux+WinJS v0.0.0
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/grunt/aliases.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | test: ['karma:test'],
5 | watch: ['karma:watch'],
6 | serve: ['concurrent:servers'],
7 |
8 | 'release': ['release-web', 'release-electron'],
9 | 'release-web': ['webpack:release-web', 'legal-eagle:web'],
10 | 'release-electron': ['webpack:release-electron', 'legal-eagle:electron'],
11 |
12 | default: ['test']
13 | }
14 |
--------------------------------------------------------------------------------
/grunt/clean.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | coverage: ['coverage/'],
5 | tmp: ['dist/tmp/'],
6 | web: ['dist/web/'],
7 | electron: ['dist/electron/']
8 | }
9 |
--------------------------------------------------------------------------------
/grunt/concurrent.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | servers: {
5 | tasks: ['webpack-dev-server:front', 'connect:back:keepalive'],
6 | options: {
7 | logConcurrentOutput: true
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/grunt/connect.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var once = require('lodash/function/once')
4 |
5 | var initStore = once(function () {
6 | return require('fortune-example')({
7 | users: {min: 30, max: 50},
8 | groups: {min: 10, max: 30},
9 | threads: {min: 1, max: 10},
10 | messages: {min: 1, max: 10}
11 | })
12 | })
13 |
14 | var extractChanges = function (methods, evt) {
15 | return ['create', 'update', 'delete'].reduce(function (memo, method) {
16 | var value = evt[methods[method]]
17 | if (value) {
18 | memo[method] = value
19 | }
20 | return memo
21 | }, {})
22 | }
23 |
24 | var enableCORS = function (req, res, next) {
25 | res.setHeader('Access-Control-Allow-Origin', '*')
26 | res.setHeader('Access-Control-Allow-Methods', '*')
27 | next()
28 | }
29 |
30 | module.exports = function () {
31 | return {
32 | back: {
33 | options: {
34 | port: 8001,
35 |
36 | middleware: function (connect, options, middlewares) {
37 | var store = initStore()
38 | var httpOptions = {json: {spaces: 2}}
39 | var http = store.http(httpOptions)
40 |
41 | middlewares.unshift(['/api', http])
42 | middlewares.unshift(enableCORS)
43 |
44 | return middlewares
45 | },
46 |
47 | onCreateServer: function (server) {
48 | var store = initStore()
49 | var wsOptions = {server: server, path: '/realtime'}
50 | var change = function (ws, evt) {
51 | ws.send(JSON.stringify(extractChanges(store.methods, evt)))
52 | }
53 | var wss = store.ws(wsOptions, change)
54 |
55 | wss.on('connection', function (ws) {
56 | ws.on('message', function (msg) {
57 | ws.send(msg)
58 | })
59 | })
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/grunt/karma.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var assign = require('lodash/object/assign')
4 | var webpack = require('webpack')
5 |
6 | var config = require('./webpack-config')
7 |
8 | var assignPlugins = function (options) {
9 | return assign(options, {
10 | plugins: [
11 | new webpack.DefinePlugin({
12 | 'process.env.NODE_ENV': '"test"'
13 | })
14 | ].concat(options.plugins)
15 | })
16 | }
17 |
18 | module.exports = {
19 | options: {
20 | basePath: '',
21 |
22 | files: ['tests.js'],
23 |
24 | browsers: ['PhantomJS'],
25 | frameworks: ['jasmine'],
26 |
27 | preprocessors: {
28 | 'tests.js': ['webpack', 'sourcemap']
29 | },
30 |
31 | webpackMiddleware: {
32 | noInfo: true
33 | },
34 |
35 | webpackPort: 9874,
36 | runnerPort: 9875,
37 | port: 9876
38 | },
39 |
40 | // Watch for changes and run all tests.
41 | watch: {
42 | webpack: assignPlugins(config({test: true})),
43 |
44 | reporters: ['dots']
45 | },
46 |
47 | // Run tests with coverage.
48 | test: {
49 | singleRun: true,
50 |
51 | webpack: assignPlugins(config({test: true, coverage: true})),
52 |
53 | reporters: ['dots', 'coverage'],
54 |
55 | coverageReporter: {
56 | dir: 'coverage',
57 |
58 | reporters: [
59 | {type: 'html', subdir: 'report-html'},
60 | {type: 'lcov', subdir: 'report-lcov'},
61 | {type: 'text'}
62 | ]
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/grunt/legal-eagle.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | options: {
5 | overrides: {
6 | 'base64id@0.1.0': {
7 | repository: 'https://github.com/faeldt/base64id',
8 | license: 'MIT'
9 | },
10 | 'deref@0.3.0': {
11 | repository: 'https://github.com/gextech/deref',
12 | license: 'MIT'
13 | },
14 | 'extract-opts@3.0.1': {
15 | repository: 'https://github.com/isaacs/inherits',
16 | license: 'ISC'
17 | },
18 | 'jsonify@0.0.0': {
19 | repository: 'https://github.com/substack/jsonify',
20 | license: 'WTF'
21 | },
22 | 'log-driver@1.2.4': {
23 | repository: 'https://github.com/cainus/logdriver',
24 | license: 'ISC'
25 | },
26 | 'rc@1.1.2': {
27 | repository: 'https://github.com/dominictarr/rc',
28 | license: 'BSD'
29 | },
30 | 'ripemd160@0.2.0': {
31 | repository: 'https://github.com/cryptocoinjs/ripemd160',
32 | license: 'BSD'
33 | },
34 | 'shelljs@0.3.0': {
35 | repository: 'https://github.com/shelljs/shelljs',
36 | license: 'BSD'
37 | },
38 | 'uri-templates@0.1.9': {
39 | repository: 'https://github.com/geraintluff/uri-templates',
40 | license: 'Unlicense'
41 | }
42 | }
43 | },
44 |
45 | web: {
46 | src: '.',
47 | dest: 'dist/web/LICENSE'
48 | },
49 |
50 | electron: {
51 | src: '.',
52 | dest: 'dist/electron/LICENSE'
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/grunt/webpack-config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var path = require('path')
4 | var autoprefixer = require('autoprefixer')
5 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
6 |
7 | var pkg = require('../package.json')
8 |
9 | module.exports = function (options) {
10 | return {
11 | output: {
12 | path: './dist/tmp/',
13 | filename: 'index.js'
14 | },
15 |
16 | debug: (options.serve || options.test),
17 | devtool: (options.serve || options.test ? 'eval' : null),
18 |
19 | resolve: {
20 | extensions: ['', '.js', '.jsx', '.json'],
21 |
22 | modulesDirectories: [
23 | 'apps',
24 | 'containers',
25 | 'components',
26 | 'actions',
27 | 'reducers',
28 | 'selectors',
29 | 'decorators',
30 | 'constants',
31 | 'styles',
32 | 'utils',
33 | 'web_modules',
34 | 'node_modules'
35 | ],
36 |
37 | alias: {
38 | 'package$': path.resolve(__dirname, '../package.json')
39 | }
40 | },
41 |
42 | resolveLoader: {
43 | extensions: ['', '.js'],
44 |
45 | modulesDirectories: [
46 | 'loaders',
47 | 'web_modules',
48 | 'node_modules'
49 | ]
50 | },
51 |
52 | plugins: [
53 | new ExtractTextPlugin('index.css', {disable: options.serve})
54 | ],
55 |
56 | module: {
57 | loaders: [
58 | {
59 | test: /\.ttf$/,
60 | loader: 'file'
61 | },
62 | {
63 | test: /\.html$/,
64 | exclude: /node_modules[\\\/]/,
65 | loader: [
66 | 'file?name=[name].[ext]',
67 | 'template'
68 | ].join('!')
69 | },
70 | {
71 | test: /\.css$/,
72 | exclude: /node_modules[\\\/]/,
73 | loader: ExtractTextPlugin.extract('style', [
74 | 'css?sourceMap'
75 | ].join('!'))
76 | },
77 | {
78 | test: /\.less$/,
79 | exclude: /node_modules[\\\/]/,
80 | loader: ExtractTextPlugin.extract('style', [
81 | 'css?sourceMap',
82 | 'postcss?sourceMap',
83 | 'less?sourceMap',
84 | 'wrap?less'
85 | ].join('!'))
86 | },
87 | {
88 | test: /\.(res)?json$/,
89 | loader: [
90 | 'json',
91 | 'template'
92 | ].join('!')
93 | },
94 | {
95 | test: /\.jsx?$/,
96 | exclude: /node_modules[\\\/]/,
97 | loader: (options.serve ? [
98 | 'react-hot'
99 | ] : []).concat([
100 | 'babel'
101 | ]).join('!')
102 | },
103 | {
104 | test: require.resolve('react/addons'),
105 | loader: 'expose?React'
106 | },
107 | {
108 | test: require.resolve('react-winjs'),
109 | loader: 'imports?WinJS=winjs'
110 | }
111 | ],
112 |
113 | postLoaders: (options.coverage ? [
114 | {
115 | test: /\.jsx?$/,
116 | exclude: /(node_modules|test)[\\\/]/,
117 | loader: 'istanbul-instrumenter'
118 | }
119 | ] : [])
120 | },
121 |
122 | package: options.package || pkg,
123 |
124 | postcss: [
125 | autoprefixer
126 | ],
127 |
128 | wrap: {
129 | less: {
130 | before: '@import "~config.less";'
131 | }
132 | },
133 |
134 | progress: true,
135 |
136 | stats: {
137 | colors: true,
138 | modules: true,
139 | reasons: true
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/grunt/webpack-dev-server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var assign = require('lodash/object/assign')
4 | var webpack = require('webpack')
5 |
6 | var config = require('./webpack-config')
7 |
8 | module.exports = function (grunt, options) {
9 | var app = grunt.option('app') || 'Desktop'
10 | var commonOptions = config(assign({serve: true}, options))
11 | assign(commonOptions, {
12 | entry: './index.js?' + app,
13 |
14 | plugins: [
15 | new webpack.DefinePlugin({
16 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
17 | })
18 | ].concat(commonOptions.plugins)
19 | })
20 |
21 | var https = false
22 | var host = '0.0.0.0'
23 | var port = 8000
24 |
25 | return {
26 | options: {
27 | webpack: commonOptions
28 | },
29 |
30 | front: {
31 | https: https,
32 | host: host,
33 | port: port,
34 |
35 | hot: true,
36 | inline: true,
37 | keepalive: true,
38 |
39 | webpack: {
40 | failOnError: false
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/grunt/webpack.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var assign = require('lodash/object/assign')
4 | var webpack = require('webpack')
5 | var StatsPlugin = require('stats-webpack-plugin')
6 |
7 | var config = require('./webpack-config')
8 |
9 | module.exports = function (grunt, options) {
10 | var app = grunt.option('app') || 'Desktop'
11 | var commonOptions = config(options)
12 | assign(commonOptions, {entry: './index.js?' + app})
13 |
14 | var watchOptions = {
15 | failOnError: false,
16 |
17 | watch: true,
18 | keepalive: true,
19 |
20 | plugins: [
21 | new webpack.DefinePlugin({
22 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
23 | })
24 | ].concat(commonOptions.plugins)
25 | }
26 |
27 | var buildOptions = {
28 | failOnError: true,
29 |
30 | plugins: [
31 | new webpack.DefinePlugin({
32 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
33 | }),
34 |
35 | new StatsPlugin('stats.json', {
36 | chunkModules: true
37 | })
38 | ].concat(commonOptions.plugins)
39 | }
40 |
41 | var releaseOptions = {
42 | failOnError: true,
43 |
44 | plugins: [
45 | new webpack.DefinePlugin({
46 | 'process.env.NODE_ENV': '"production"'
47 | }),
48 |
49 | new webpack.optimize.UglifyJsPlugin(),
50 | new webpack.optimize.OccurenceOrderPlugin(),
51 |
52 | new StatsPlugin('stats.json', {
53 | chunkModules: true
54 | })
55 | ].concat(commonOptions.plugins)
56 | }
57 |
58 | return {
59 | options: commonOptions,
60 |
61 | // Unoptimized build.
62 | build: buildOptions,
63 |
64 | // Watch for changes and do an unoptimized build.
65 | watch: watchOptions,
66 |
67 | // Optimized build for web.
68 | 'release-web': assign({
69 | target: 'web',
70 |
71 | output: {
72 | path: './dist/web/',
73 | filename: 'index.js'
74 | }
75 | }, releaseOptions),
76 |
77 | // Optimized build for electron.
78 | 'release-electron': assign({
79 | target: 'electron',
80 |
81 | output: {
82 | path: './dist/electron/',
83 | filename: 'index.js'
84 | }
85 | }, releaseOptions)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*eslint-disable no-undef */
4 | require(__resourceQuery.substr(1))
5 |
--------------------------------------------------------------------------------
/loaders/template.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var _ = require('lodash')
4 | var utils = require('loader-utils')
5 |
6 | module.exports = function (content) {
7 | if (this.cacheable != null) {
8 | this.cacheable()
9 | }
10 |
11 | var query = utils.parseQuery(this.query)
12 | var opts = this.options
13 |
14 | return _.template(content)(_.extend(query, opts))
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-winjs-example",
3 | "productName": "React+Redux+WinJS",
4 | "description": "Example app using React, Redux and WinJS.",
5 | "version": "0.0.0",
6 | "license": "MIT",
7 |
8 | "private": true,
9 | "main": "index.js",
10 |
11 | "author": {
12 | "name" : "Daniel Perez Alvarez",
13 | "email" : "unindented@gmail.com",
14 | "url" : "https://unindented.org/"
15 | },
16 |
17 | "repository": {
18 | "type": "git",
19 | "url": "git@github.com:unindented/react-redux-winjs-example.git"
20 | },
21 |
22 | "scripts": {
23 | "start": "grunt serve",
24 | "test": "grunt test",
25 | "release": "grunt release",
26 | "publish": "git subtree push --prefix dist/web/ origin gh-pages"
27 | },
28 |
29 | "dependencies": {
30 | "history": "^1.9.0",
31 | "immutable": "^3.7.5",
32 | "lodash": "^3.10.1",
33 | "react": "^0.13.3",
34 | "react-immutable-proptypes": "^1.2.0",
35 | "react-redux": "^3.0.1",
36 | "react-router": "^1.0.0-rc1",
37 | "react-winjs": "^2.4.0",
38 | "redux": "^3.0.0",
39 | "redux-thunk": "^1.0.0",
40 | "reselect": "^2.0.0",
41 | "shallowequal": "^0.2.2",
42 | "whatwg-fetch": "^0.9.0",
43 | "winjs": "^4.3.0"
44 | },
45 |
46 | "devDependencies": {
47 | "autoprefixer": "^6.0.0",
48 | "babel-core": "^5.8.25",
49 | "babel-runtime": "^5.8.25",
50 | "coveralls": "^2.11.4",
51 | "eslint": "^1.6.0",
52 | "eslint-config-standard": "^4.4.0",
53 | "eslint-config-standard-react": "^1.1.0",
54 | "eslint-plugin-standard": "^1.3.0",
55 | "eslint-plugin-react": "^3.3.1",
56 | "jasmine-core": "^2.3.4",
57 | "jasmine-jquery-matchers": "^1.0.0",
58 | "jasmine-immutable-matchers": "^0.1.0",
59 | "jquery": "^2.1.4",
60 | "less": "^2.5.3",
61 | "node-libs-browser": "^0.5.3",
62 | "phantomjs": "^1.9.18",
63 | "webpack": "^1.12.2",
64 | "webpack-dev-server": "^1.12.0",
65 |
66 | "loader-utils": "^0.2.11",
67 |
68 | "babel-loader": "^5.3.2",
69 | "css-loader": "^0.19.0",
70 | "exports-loader": "^0.6.2",
71 | "expose-loader": "^0.7.0",
72 | "file-loader": "^0.8.4",
73 | "imports-loader": "^0.6.4",
74 | "istanbul-instrumenter-loader": "^0.1.3",
75 | "json-loader": "^0.5.3",
76 | "less-loader": "^2.2.1",
77 | "postcss-loader": "^0.6.0",
78 | "react-hot-loader": "^1.3.0",
79 | "style-loader": "^0.12.3",
80 | "wrap-loader": "^0.1.0",
81 |
82 | "extract-text-webpack-plugin": "^0.8.2",
83 | "stats-webpack-plugin": "^0.2.2",
84 |
85 | "karma": "^0.13.10",
86 | "karma-coverage": "^0.5.1",
87 | "karma-jasmine": "^0.3.6",
88 | "karma-sourcemap-loader": "^0.3.5",
89 | "karma-webpack": "^1.7.0",
90 |
91 | "karma-phantomjs-launcher": "^0.2.1",
92 | "karma-chrome-launcher": "^0.2.1",
93 | "karma-firefox-launcher": "^0.1.6",
94 | "karma-opera-launcher": "^0.3.0",
95 | "karma-safari-launcher": "^0.1.1",
96 | "karma-ie-launcher": "^0.2.0",
97 |
98 | "grunt": "^0.4.5",
99 | "grunt-concurrent": "^2.0.3",
100 | "grunt-contrib-clean": "^0.6.0",
101 | "grunt-contrib-connect": "^0.11.2",
102 | "grunt-legal-eagle": "^0.2.0",
103 | "grunt-karma": "^0.12.1",
104 | "grunt-webpack": "^1.0.11",
105 |
106 | "load-grunt-config": "^0.17.2",
107 |
108 | "fortune-example": "unindented/fortune-example"
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/reducers/AllReducers/index.js:
--------------------------------------------------------------------------------
1 | export {default as groups} from 'groupsReducer'
2 | export {default as selectedGroupId} from 'selectedGroupReducer'
3 | export {default as users} from 'usersReducer'
4 | export {default as selectedUserId} from 'selectedUserReducer'
5 |
--------------------------------------------------------------------------------
/reducers/groupsReducer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/groupsReducer'
2 |
--------------------------------------------------------------------------------
/reducers/groupsReducer/src/groupsReducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import {ADD_GROUP, REMOVE_GROUP, UPDATE_GROUP, GET_GROUPS} from 'ActionTypes'
4 |
5 | const initialState = Immutable.Map()
6 |
7 | export default (state = initialState, action = {}) => {
8 | if (action.error) {
9 | console.warn(action.payload.message)
10 | return state
11 | }
12 |
13 | switch (action.type) {
14 | case ADD_GROUP:
15 | return state.set(action.payload.get('id'), action.payload)
16 |
17 | case REMOVE_GROUP:
18 | return state.delete(action.payload.get('id'))
19 |
20 | case UPDATE_GROUP:
21 | return state.set(action.payload.get('id'), action.payload)
22 |
23 | case GET_GROUPS:
24 | return action.payload
25 |
26 | default:
27 | return state
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/reducers/groupsReducer/test/groupsReducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import {ADD_GROUP, REMOVE_GROUP, UPDATE_GROUP, GET_GROUPS} from 'ActionTypes'
4 | import groups from 'groupsReducer'
5 |
6 | describe('groupsReducer', () => {
7 | const state = Immutable.fromJS({
8 | '0': {id: '0', name: 'Foo'},
9 | '1': {id: '1', name: 'Bar'}
10 | })
11 |
12 | describe('DEFAULT', () => {
13 | it('returns the same state', function () {
14 | expect(groups(state)).toBe(state)
15 | })
16 | })
17 |
18 | describe('ADD_GROUP', () => {
19 | it('adds a new group', function () {
20 | const payload = Immutable.fromJS({
21 | id: '2', name: 'Baz'
22 | })
23 | const action = {
24 | type: ADD_GROUP,
25 | payload: payload
26 | }
27 |
28 | expect(groups(state, action))
29 | .toEqualImmutable(state.set(payload.get('id'), payload))
30 | })
31 | })
32 |
33 | describe('REMOVE_GROUP', () => {
34 | it('removes an existing group', function () {
35 | const payload = state.get('1')
36 | const action = {
37 | type: REMOVE_GROUP,
38 | payload: payload
39 | }
40 |
41 | expect(groups(state, action))
42 | .toEqualImmutable(state.delete(payload.get('id')))
43 | })
44 | })
45 |
46 | describe('UPDATE_GROUP', () => {
47 | it('updates an existing group', function () {
48 | const payload = state.get('1').set('name', 'Rab')
49 | const action = {
50 | type: UPDATE_GROUP,
51 | payload: payload
52 | }
53 |
54 | expect(groups(state, action))
55 | .toEqualImmutable(state.set(payload.get('id'), payload))
56 | })
57 | })
58 |
59 | describe('GET_GROUPS', () => {
60 | it('replaces all groups', function () {
61 | const payload = Immutable.fromJS({
62 | '2': {id: '2', name: 'Baz'},
63 | '3': {id: '3', name: 'Qux'}
64 | })
65 | const action = {
66 | type: GET_GROUPS,
67 | payload: payload
68 | }
69 |
70 | expect(groups(state, action)).toEqualImmutable(payload)
71 | })
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/reducers/selectedGroupReducer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/selectedGroupReducer'
2 |
--------------------------------------------------------------------------------
/reducers/selectedGroupReducer/src/selectedGroupReducer.js:
--------------------------------------------------------------------------------
1 | import {SELECT_GROUP} from 'ActionTypes'
2 |
3 | export default (state = null, action = {}) => {
4 | if (action.error) {
5 | console.warn(action.payload.message)
6 | return state
7 | }
8 |
9 | switch (action.type) {
10 | case SELECT_GROUP:
11 | return action.payload
12 |
13 | default:
14 | return state
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/reducers/selectedGroupReducer/test/selectedGroupReducer.js:
--------------------------------------------------------------------------------
1 | import {SELECT_GROUP} from 'ActionTypes'
2 | import selectedGroupId from 'selectedGroupReducer'
3 |
4 | describe('selectedGroupReducer', () => {
5 | const state = '1'
6 |
7 | describe('DEFAULT', () => {
8 | it('returns the same state', function () {
9 | expect(selectedGroupId(state)).toBe(state)
10 | })
11 | })
12 |
13 | describe('SELECT_GROUP', () => {
14 | it('returns the new selected group ID', function () {
15 | const payload = '2'
16 | const action = {
17 | type: SELECT_GROUP,
18 | payload: payload
19 | }
20 |
21 | expect(selectedGroupId(state, action)).toBe(payload)
22 | })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/reducers/selectedUserReducer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/selectedUserReducer'
2 |
--------------------------------------------------------------------------------
/reducers/selectedUserReducer/src/selectedUserReducer.js:
--------------------------------------------------------------------------------
1 | import {SELECT_USER} from 'ActionTypes'
2 |
3 | export default (state = null, action = {}) => {
4 | if (action.error) {
5 | console.warn(action.payload.message)
6 | return state
7 | }
8 |
9 | switch (action.type) {
10 | case SELECT_USER:
11 | return action.payload
12 |
13 | default:
14 | return state
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/reducers/selectedUserReducer/test/selectedUserReducer.js:
--------------------------------------------------------------------------------
1 | import {SELECT_USER} from 'ActionTypes'
2 | import selectedUserId from 'selectedUserReducer'
3 |
4 | describe('selectedUserReducer', () => {
5 | const state = '1'
6 |
7 | describe('DEFAULT', () => {
8 | it('returns the same state', function () {
9 | expect(selectedUserId(state)).toBe(state)
10 | })
11 | })
12 |
13 | describe('SELECT_USER', () => {
14 | it('returns the new selected user ID', function () {
15 | const payload = '2'
16 | const action = {
17 | type: SELECT_USER,
18 | payload: payload
19 | }
20 |
21 | expect(selectedUserId(state, action)).toEqualImmutable(payload)
22 | })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/reducers/usersReducer/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/usersReducer'
2 |
--------------------------------------------------------------------------------
/reducers/usersReducer/src/usersReducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import {ADD_USER, REMOVE_USER, UPDATE_USER, GET_USERS} from 'ActionTypes'
4 |
5 | const initialState = Immutable.Map()
6 |
7 | export default (state = initialState, action = {}) => {
8 | if (action.error) {
9 | console.warn(action.payload.message)
10 | return state
11 | }
12 |
13 | switch (action.type) {
14 | case ADD_USER:
15 | return state.set(action.payload.get('id'), action.payload)
16 |
17 | case REMOVE_USER:
18 | return state.delete(action.payload.get('id'))
19 |
20 | case UPDATE_USER:
21 | return state.set(action.payload.get('id'), action.payload)
22 |
23 | case GET_USERS:
24 | return action.payload
25 |
26 | default:
27 | return state
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/reducers/usersReducer/test/usersReducer.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import {ADD_USER, REMOVE_USER, UPDATE_USER, GET_USERS} from 'ActionTypes'
4 | import users from 'usersReducer'
5 |
6 | describe('usersReducer', () => {
7 | const state = Immutable.fromJS({
8 | '0': {id: '0', firstName: 'Foo', lastName: 'Bar'},
9 | '1': {id: '1', firstName: 'Foo', lastName: 'Baz'}
10 | })
11 |
12 | describe('DEFAULT', () => {
13 | it('returns the same state', function () {
14 | expect(users(state)).toBe(state)
15 | })
16 | })
17 |
18 | describe('ADD_USER', () => {
19 | it('adds a new user', function () {
20 | const payload = Immutable.fromJS({
21 | id: '2', firstName: 'Foo', lastName: 'Qux'
22 | })
23 | const action = {
24 | type: ADD_USER,
25 | payload: payload
26 | }
27 |
28 | expect(users(state, action))
29 | .toEqualImmutable(state.set(payload.get('id'), payload))
30 | })
31 | })
32 |
33 | describe('REMOVE_USER', () => {
34 | it('removes an existing user', function () {
35 | const payload = state.get('1')
36 | const action = {
37 | type: REMOVE_USER,
38 | payload: payload
39 | }
40 |
41 | expect(users(state, action))
42 | .toEqualImmutable(state.delete(payload.get('id')))
43 | })
44 | })
45 |
46 | describe('UPDATE_USER', () => {
47 | it('updates an existing user', function () {
48 | const payload = state.get('1').set('lastName', 'Rab')
49 | const action = {
50 | type: UPDATE_USER,
51 | payload: payload
52 | }
53 |
54 | expect(users(state, action))
55 | .toEqualImmutable(state.set(payload.get('id'), payload))
56 | })
57 | })
58 |
59 | describe('GET_USERS', () => {
60 | it('replaces all users', function () {
61 | const payload = Immutable.fromJS({
62 | '2': {id: '2', firstName: 'Foo', lastName: 'Qux'},
63 | '3': {id: '3', firstName: 'Foo', lastName: 'Quux'}
64 | })
65 | const action = {
66 | type: GET_USERS,
67 | payload: payload
68 | }
69 |
70 | expect(users(state, action)).toEqualImmutable(payload)
71 | })
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/selectors/groupsSelector/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/groupsSelector'
2 |
--------------------------------------------------------------------------------
/selectors/groupsSelector/src/groupsSelector.js:
--------------------------------------------------------------------------------
1 | export default (state) => state.groups
2 |
--------------------------------------------------------------------------------
/selectors/groupsSelector/test/groupsSelector.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import groupsSelector from 'groupsSelector'
4 |
5 | describe('groupsSelector', () => {
6 | const state = {
7 | groups: Immutable.fromJS({
8 | '0': {id: '0', name: 'Foo'},
9 | '1': {id: '1', name: 'Bar'}
10 | })
11 | }
12 |
13 | it('selects all groups', function () {
14 | expect(groupsSelector(state)).toBe(state.groups)
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/selectors/selectedGroupSelector/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/selectedGroupSelector'
2 |
--------------------------------------------------------------------------------
/selectors/selectedGroupSelector/src/selectedGroupSelector.js:
--------------------------------------------------------------------------------
1 | import {createSelector} from 'reselect'
2 |
3 | import groupsSelector from 'groupsSelector'
4 |
5 | const selectedGroupIdSelector = (state) => state.selectedGroupId
6 |
7 | export default createSelector(
8 | [groupsSelector, selectedGroupIdSelector],
9 | (groups, selectedGroupId) => groups.get(selectedGroupId)
10 | )
11 |
--------------------------------------------------------------------------------
/selectors/selectedGroupSelector/test/selectedGroupSelector.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import selectedGroupSelector from 'selectedGroupSelector'
4 |
5 | describe('selectedGroupSelector', () => {
6 | const state = {
7 | groups: Immutable.fromJS({
8 | '0': {id: '0', name: 'Foo'},
9 | '1': {id: '1', name: 'Bar'}
10 | }),
11 | selectedGroupId: '1'
12 | }
13 |
14 | it('selects the selected group', function () {
15 | expect(selectedGroupSelector(state)).toBe(state.groups.get('1'))
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/selectors/selectedUserSelector/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/selectedUserSelector'
2 |
--------------------------------------------------------------------------------
/selectors/selectedUserSelector/src/selectedUserSelector.js:
--------------------------------------------------------------------------------
1 | import {createSelector} from 'reselect'
2 |
3 | import usersSelector from 'usersSelector'
4 |
5 | const selectedUserIdSelector = (state) => state.selectedUserId
6 |
7 | export default createSelector(
8 | [usersSelector, selectedUserIdSelector],
9 | (users, selectedUserId) => users.get(selectedUserId)
10 | )
11 |
--------------------------------------------------------------------------------
/selectors/selectedUserSelector/test/selectedUserSelector.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import selectedUserSelector from 'selectedUserSelector'
4 |
5 | describe('selectedUserSelector', () => {
6 | const state = {
7 | users: Immutable.fromJS({
8 | '0': {id: '0', name: 'Foo'},
9 | '1': {id: '1', name: 'Bar'}
10 | }),
11 | selectedUserId: '1'
12 | }
13 |
14 | it('selects the selected user', function () {
15 | expect(selectedUserSelector(state)).toBe(state.users.get('1'))
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/selectors/usersSelector/index.js:
--------------------------------------------------------------------------------
1 | export default from './src/usersSelector'
2 |
--------------------------------------------------------------------------------
/selectors/usersSelector/src/usersSelector.js:
--------------------------------------------------------------------------------
1 | export default (state) => state.users
2 |
--------------------------------------------------------------------------------
/selectors/usersSelector/test/usersSelector.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import usersSelector from 'usersSelector'
4 |
5 | describe('usersSelector', () => {
6 | const state = {
7 | users: Immutable.fromJS({
8 | '0': {id: '0', name: 'Foo'},
9 | '1': {id: '1', name: 'Bar'}
10 | })
11 | }
12 |
13 | it('selects all users', function () {
14 | expect(usersSelector(state)).toBe(state.users)
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/styles/base.less:
--------------------------------------------------------------------------------
1 | @import "reset.less";
2 | @import "~winjs/css/ui-light.css";
3 |
4 | /**
5 | * Style body as a flex container.
6 | */
7 |
8 | body {
9 | display: flex;
10 | }
11 |
12 | /**
13 | * Style list view to avoid hardcoded height.
14 | */
15 |
16 | .win-listview {
17 | height: auto;
18 | overflow: auto;
19 | position: absolute !important;
20 | top: 0;
21 | right: 0;
22 | bottom: 0;
23 | left: 0;
24 | }
25 |
26 | /**
27 | * Style headers in grouped lists so that the show up as small squares.
28 | */
29 |
30 | .win-listview.win-groups > .win-vertical .win-listlayout .win-groupheadercontainer {
31 | margin: @default-padding;
32 | }
33 |
34 | .win-listview.win-groups > .win-vertical .win-listlayout .win-groupheader {
35 | display: block;
36 | font-size: 15px;
37 | line-height: @list-header-height;
38 | padding: 0;
39 | text-align: center;
40 | width: @list-header-width;
41 | }
42 |
43 | .win-listview.win-groups > .win-vertical .win-surface.win-listlayout {
44 | margin-top: 0;
45 | }
46 |
47 | /**
48 | * Style items in lists.
49 | */
50 | .win-listview > .win-vertical .win-listlayout .win-container {
51 | margin: 0;
52 | }
53 |
54 | .win-listview > .win-vertical .win-listlayout .win-item {
55 | cursor: pointer;
56 | height: @list-item-height;
57 | padding: 0 @default-padding;
58 | width: 100%;
59 | }
60 |
61 | /**
62 | * Show a pointer cursor for the split view toggle.
63 | */
64 |
65 | .win-splitviewpanetoggle {
66 | cursor: pointer;
67 | }
68 |
69 | /**
70 | * Style the split view pane and its buttons.
71 | */
72 |
73 | .win-splitview-pane {
74 | background-color: @navigation-background-color;
75 | display: flex;
76 | }
77 |
78 | .win-splitview-pane .win-navbarcommand-button,
79 | .win-splitview-pane .win-navbarcommand-splitbutton {
80 | background-color: transparent;
81 | }
82 |
--------------------------------------------------------------------------------
/styles/config.less:
--------------------------------------------------------------------------------
1 | /**
2 | * Colors
3 | */
4 | @brand-color: #0072c6;
5 | @subtle-color: #777777;
6 |
7 | /**
8 | * Margins and paddings.
9 | */
10 | @default-margin: 12px;
11 | @default-padding: 12px;
12 |
13 | /**
14 | * Header bar.
15 | */
16 |
17 | @header-background-color: @brand-color;
18 | @header-height: 48px;
19 |
20 | /**
21 | * Navigation pane.
22 | */
23 | @navigation-background-color: darken(white, 10%);
24 | @navigation-width: 48px;
25 |
26 | /**
27 | * Sidebar pane.
28 | */
29 | @sidebar-width: (320px - @navigation-width);
30 |
31 | /**
32 | * List headers.
33 | */
34 | @list-header-background-color: @brand-color;
35 | @list-header-text-color: white;
36 | @list-header-height: 32px;
37 | @list-header-width: 32px;
38 |
39 | /**
40 | * List items.
41 | */
42 | @list-item-height: @navigation-width;
43 |
44 | /**
45 | * List avatars.
46 | */
47 | @list-avatar-border-radius: 50%;
48 | @list-avatar-margin: (@list-item-height - @list-avatar-height) / 2 0;
49 | @list-avatar-width: 32px;
50 | @list-avatar-height: @list-avatar-width;
51 |
52 | /**
53 | * Page title.
54 | */
55 | @page-title-padding: @default-padding * 2;
56 |
57 | /**
58 | * Page avatars.
59 | */
60 | @page-avatar-border-radius: 50%;
61 | @page-avatar-margin: 0;
62 | @page-avatar-width: (@list-item-height - @default-padding) * 2;
63 | @page-avatar-height: @page-avatar-width;
64 |
--------------------------------------------------------------------------------
/styles/reset.less:
--------------------------------------------------------------------------------
1 | /* Reset
2 | ========================================================================== */
3 |
4 | html, body, div, span, applet, object, iframe,
5 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
6 | a, abbr, acronym, address, big, cite, code,
7 | del, dfn, em, img, ins, kbd, q, s, samp,
8 | small, strike, strong, sub, sup, tt, var,
9 | b, u, i, center,
10 | dl, dt, dd, ol, ul, li,
11 | fieldset, form, label, legend,
12 | table, caption, tbody, tfoot, thead, tr, th, td,
13 | article, aside, canvas, details, embed,
14 | figure, figcaption, footer, header, hgroup,
15 | menu, nav, output, ruby, section, summary,
16 | time, mark, audio, video {
17 | margin: 0;
18 | padding: 0;
19 | border: 0;
20 | font-size: 100%;
21 | font: inherit;
22 | vertical-align: baseline;
23 | }
24 |
25 | article, aside, details, figcaption, figure,
26 | footer, header, hgroup, menu, nav, section {
27 | display: block;
28 | }
29 |
30 | body {
31 | line-height: 1;
32 | }
33 |
34 | ol, ul {
35 | list-style: none;
36 | }
37 |
38 | blockquote, q {
39 | quotes: none;
40 |
41 | &:before, &:after {
42 | content: "";
43 | content: none;
44 | }
45 | }
46 |
47 | table {
48 | border-collapse: collapse;
49 | border-spacing: 0;
50 | }
51 |
52 | html {
53 | box-sizing: border-box;
54 | }
55 | *, *:before, *:after {
56 | box-sizing: inherit;
57 | }
58 |
--------------------------------------------------------------------------------
/tests.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | require('babel-core/polyfill')
4 |
5 | var jqueryMatchers = require('jasmine-jquery-matchers')
6 | var immutableMatchers = require('jasmine-immutable-matchers')
7 |
8 | beforeEach(function () {
9 | jasmine.addMatchers(jqueryMatchers)
10 | jasmine.addMatchers(immutableMatchers)
11 | })
12 |
13 | var appsContext = require.context('./apps', true, /\/test\/.*\.jsx?$/)
14 | appsContext.keys().forEach(appsContext)
15 |
16 | var containersContext = require.context('./containers', true, /\/test\/.*\.jsx?$/)
17 | containersContext.keys().forEach(containersContext)
18 |
19 | var componentsContext = require.context('./components', true, /\/test\/.*\.jsx?$/)
20 | componentsContext.keys().forEach(componentsContext)
21 |
22 | var actionsContext = require.context('./actions', true, /\/test\/.*\.jsx?$/)
23 | actionsContext.keys().forEach(actionsContext)
24 |
25 | var reducersContext = require.context('./reducers', true, /\/test\/.*\.jsx?$/)
26 | reducersContext.keys().forEach(reducersContext)
27 |
28 | var selectorsContext = require.context('./selectors', true, /\/test\/.*\.jsx?$/)
29 | selectorsContext.keys().forEach(selectorsContext)
30 |
31 | var decoratorsContext = require.context('./decorators', true, /\/test\/.*\.jsx?$/)
32 | decoratorsContext.keys().forEach(decoratorsContext)
33 |
34 | var utilsContext = require.context('./utils', true, /\/test\/.*\.jsx?$/)
35 | utilsContext.keys().forEach(utilsContext)
36 |
--------------------------------------------------------------------------------
/utils/ApiHelper/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/ApiHelper'
2 |
--------------------------------------------------------------------------------
/utils/ApiHelper/src/ApiHelper.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import pick from 'lodash/object/pick'
3 | import template from 'lodash/string/template'
4 | import 'whatwg-fetch'
5 |
6 | import {
7 | GROUPS_API_URL,
8 | GROUPS_BY_ID_API_URL,
9 | USERS_API_URL,
10 | USERS_BY_ID_API_URL,
11 | ARTIFICIAL_DELAY
12 | } from 'Config'
13 |
14 | // HELPERS /////////////////////////////////////////////////////////////////////
15 |
16 | const delay = (time) => {
17 | return !time ? null : (res) => {
18 | return new Promise((fulfill) => {
19 | setTimeout(() => {
20 | fulfill(res)
21 | }, time)
22 | })
23 | }
24 | }
25 |
26 | const jsonify = (res) => {
27 | return res.json()
28 | }
29 |
30 | const fetchApi = (url, params) => {
31 | return fetch(template(url)(params))
32 | .then(delay(ARTIFICIAL_DELAY * (Math.random() + 0.5)))
33 | .then(jsonify)
34 | }
35 |
36 | const massageEntity = (ent, props) => {
37 | // Pick attributes we're interested in.
38 | ent = pick(ent, ['µ:id', 'id'].concat(props))
39 |
40 | // Rename ID.
41 | if (ent.id == null) {
42 | ent.id = ent['µ:id']
43 | delete ent['µ:id']
44 | }
45 |
46 | // Bust image cache.
47 | if (ent.avatar != null) {
48 | ent.avatar = `${ent.avatar}?_=${ent.id}`
49 | }
50 |
51 | return ent
52 | }
53 |
54 | const massageGroup = (group) => {
55 | return massageEntity(group, [
56 | 'name',
57 | 'description',
58 | 'avatar'
59 | ])
60 | }
61 |
62 | const massageUser = (user) => {
63 | return massageEntity(user, [
64 | 'firstName',
65 | 'lastName',
66 | 'jobTitle',
67 | 'avatar'
68 | ])
69 | }
70 |
71 | const generateGroups = (json) => {
72 | return json['@graph'].reduce((memo, group) => {
73 | group = massageGroup(group)
74 | memo[group.id] = group
75 | return memo
76 | }, {})
77 | }
78 |
79 | const generateUsers = (json) => {
80 | return json['@graph'].reduce((memo, user) => {
81 | user = massageUser(user)
82 | memo[user.id] = user
83 | return memo
84 | }, {})
85 | }
86 |
87 | // EXPORTS /////////////////////////////////////////////////////////////////////
88 |
89 | export function getGroups () {
90 | return fetchApi(GROUPS_API_URL)
91 | .then((json) => (
92 | Immutable.fromJS(generateGroups(json))
93 | ))
94 | }
95 |
96 | export function getGroupsById (ids) {
97 | return fetchApi(GROUPS_BY_ID_API_URL, {ids: ids.map(encodeURIComponent)})
98 | .then((json) => (
99 | Immutable.fromJS(generateGroups(json))
100 | ))
101 | }
102 |
103 | export function getUsers () {
104 | return fetchApi(USERS_API_URL)
105 | .then((json) => (
106 | Immutable.fromJS(generateUsers(json))
107 | ))
108 | }
109 |
110 | export function getUsersById (ids) {
111 | return fetchApi(USERS_BY_ID_API_URL, {ids: ids.map(encodeURIComponent)})
112 | .then((json) => (
113 | Immutable.fromJS(generateUsers(json))
114 | ))
115 | }
116 |
--------------------------------------------------------------------------------
/utils/ApiHelper/test/ApiHelper.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import template from 'lodash/string/template'
3 |
4 | import * as ApiHelper from 'ApiHelper'
5 |
6 | import {
7 | GROUPS_API_URL,
8 | GROUPS_BY_ID_API_URL,
9 | USERS_API_URL,
10 | USERS_BY_ID_API_URL
11 | } from 'Config'
12 |
13 | describe('ApiHelper', () => {
14 | describe('#getGroups', () => {
15 | it('gets all groups', (done) => {
16 | const groups = {
17 | '1': {id: '1', name: 'Foo Bar'},
18 | '2': {id: '2', name: 'Foo Baz'}
19 | }
20 | const payload = {
21 | '@graph': Object.values(groups),
22 | '@meta': {}
23 | }
24 |
25 | spyOn(window, 'fetch').and.returnValue(
26 | Promise.resolve(new Response(JSON.stringify(payload)))
27 | )
28 |
29 | ApiHelper.getGroups()
30 | .then((res) => {
31 | expect(window.fetch).toHaveBeenCalledWith(GROUPS_API_URL)
32 | expect(res).toEqualImmutable(Immutable.fromJS(groups))
33 | done()
34 | })
35 | })
36 | })
37 |
38 | describe('#getGroupsById', () => {
39 | it('gets the groups with the specified IDs', (done) => {
40 | const groups = {
41 | '1': {id: '1', name: 'Foo Bar'},
42 | '2': {id: '2', name: 'Foo Baz'}
43 | }
44 | const payload = {
45 | '@graph': Object.values(groups),
46 | '@meta': {}
47 | }
48 |
49 | spyOn(window, 'fetch').and.returnValue(
50 | Promise.resolve(new Response(JSON.stringify(payload)))
51 | )
52 |
53 | ApiHelper.getGroupsById(['1', '2'])
54 | .then((res) => {
55 | const url = template(GROUPS_BY_ID_API_URL)({ids: ['1', '2']})
56 | expect(window.fetch).toHaveBeenCalledWith(url)
57 | expect(res).toEqualImmutable(Immutable.fromJS(groups))
58 | done()
59 | })
60 | })
61 | })
62 |
63 | describe('#getUsers', () => {
64 | it('gets all users', (done) => {
65 | const users = {
66 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
67 | '2': {id: '2', firstName: 'Foo', lastName: 'Baz'}
68 | }
69 | const payload = {
70 | '@graph': Object.values(users),
71 | '@meta': {}
72 | }
73 |
74 | spyOn(window, 'fetch').and.returnValue(
75 | Promise.resolve(new Response(JSON.stringify(payload)))
76 | )
77 |
78 | ApiHelper.getUsers()
79 | .then((res) => {
80 | expect(window.fetch).toHaveBeenCalledWith(USERS_API_URL)
81 | expect(res).toEqualImmutable(Immutable.fromJS(users))
82 | done()
83 | })
84 | })
85 | })
86 |
87 | describe('#getUsersById', () => {
88 | it('gets the user', (done) => {
89 | const users = {
90 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
91 | '2': {id: '2', firstName: 'Foo', lastName: 'Baz'}
92 | }
93 | const payload = {
94 | '@graph': Object.values(users),
95 | '@meta': {}
96 | }
97 |
98 | spyOn(window, 'fetch').and.returnValue(
99 | Promise.resolve(new Response(JSON.stringify(payload)))
100 | )
101 |
102 | ApiHelper.getUsersById(['1', '2'])
103 | .then((res) => {
104 | const url = template(USERS_BY_ID_API_URL)({ids: ['1', '2']})
105 | expect(window.fetch).toHaveBeenCalledWith(url)
106 | expect(res).toEqualImmutable(Immutable.fromJS(users))
107 | done()
108 | })
109 | })
110 | })
111 | })
112 |
--------------------------------------------------------------------------------
/utils/InfoHelper/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/InfoHelper'
2 |
--------------------------------------------------------------------------------
/utils/InfoHelper/src/InfoHelper.js:
--------------------------------------------------------------------------------
1 | import pkg from 'package'
2 |
3 | // EXPORTS /////////////////////////////////////////////////////////////////////
4 |
5 | export function getName () {
6 | return pkg.productName
7 | }
8 |
9 | export function getVersion () {
10 | return pkg.version
11 | }
12 |
--------------------------------------------------------------------------------
/utils/InfoHelper/test/InfoHelper.js:
--------------------------------------------------------------------------------
1 | import * as InfoHelper from 'InfoHelper'
2 |
3 | describe('InfoHelper', () => {
4 | describe('#getName', () => {
5 | it('returns the app name', () => {
6 | expect(InfoHelper.getName()).toBeTruthy()
7 | })
8 | })
9 |
10 | describe('#getVersion', () => {
11 | it('return the app version', () => {
12 | expect(InfoHelper.getVersion()).toMatch(/\d\.\d\.\d/)
13 | })
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/utils/RealtimeHelper/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/RealtimeHelper'
2 |
--------------------------------------------------------------------------------
/utils/RealtimeHelper/src/RealtimeHelper.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | import * as ApiHelper from 'ApiHelper'
4 | import {REALTIME_URL} from 'Config'
5 |
6 | // HELPERS /////////////////////////////////////////////////////////////////////
7 |
8 | class WebSocketWrapper {
9 | constructor () {
10 | this.socket = new WebSocket(REALTIME_URL)
11 | }
12 |
13 | on (type, callback) {
14 | this.socket[`on${type}`] = callback
15 | }
16 |
17 | close () {
18 | this.socket.close()
19 | }
20 | }
21 |
22 | // EXPORTS /////////////////////////////////////////////////////////////////////
23 |
24 | export function createConnection () {
25 | return new WebSocketWrapper()
26 | }
27 |
28 | export function getGroupsById (ids) {
29 | return fetchApi(GROUPS_BY_ID_API_URL, {ids: ids.map(encodeURIComponent)})
30 | .then((json) => (
31 | Immutable.fromJS(generateGroups(json))
32 | ))
33 | }
34 |
35 | export function getUsers () {
36 | return fetchApi(USERS_API_URL)
37 | .then((json) => (
38 | Immutable.fromJS(generateUsers(json))
39 | ))
40 | }
41 |
42 | export function getUsersById (ids) {
43 | return fetchApi(USERS_BY_ID_API_URL, {ids: ids.map(encodeURIComponent)})
44 | .then((json) => (
45 | Immutable.fromJS(generateUsers(json))
46 | ))
47 | }
48 |
--------------------------------------------------------------------------------
/utils/RealtimeHelper/test/RealtimeHelper.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import template from 'lodash/string/template'
3 |
4 | import * as ApiHelper from 'ApiHelper'
5 |
6 | import {
7 | GROUPS_API_URL,
8 | GROUPS_BY_ID_API_URL,
9 | USERS_API_URL,
10 | USERS_BY_ID_API_URL
11 | } from 'Config'
12 |
13 | describe('ApiHelper', () => {
14 | describe('#getGroups', () => {
15 | it('gets all groups', (done) => {
16 | const groups = {
17 | '1': {id: '1', name: 'Foo Bar'},
18 | '2': {id: '2', name: 'Foo Baz'}
19 | }
20 | const payload = {
21 | '@graph': Object.values(groups),
22 | '@meta': {}
23 | }
24 |
25 | spyOn(window, 'fetch').and.returnValue(
26 | Promise.resolve(new Response(JSON.stringify(payload)))
27 | )
28 |
29 | ApiHelper.getGroups()
30 | .then((res) => {
31 | expect(window.fetch).toHaveBeenCalledWith(GROUPS_API_URL)
32 | expect(res).toEqualImmutable(Immutable.fromJS(groups))
33 | done()
34 | })
35 | })
36 | })
37 |
38 | describe('#getGroupsById', () => {
39 | it('gets the groups with the specified IDs', (done) => {
40 | const groups = {
41 | '1': {id: '1', name: 'Foo Bar'},
42 | '2': {id: '2', name: 'Foo Baz'}
43 | }
44 | const payload = {
45 | '@graph': Object.values(groups),
46 | '@meta': {}
47 | }
48 |
49 | spyOn(window, 'fetch').and.returnValue(
50 | Promise.resolve(new Response(JSON.stringify(payload)))
51 | )
52 |
53 | ApiHelper.getGroupsById(['1', '2'])
54 | .then((res) => {
55 | const url = template(GROUPS_BY_ID_API_URL)({ids: ['1', '2']})
56 | expect(window.fetch).toHaveBeenCalledWith(url)
57 | expect(res).toEqualImmutable(Immutable.fromJS(groups))
58 | done()
59 | })
60 | })
61 | })
62 |
63 | describe('#getUsers', () => {
64 | it('gets all users', (done) => {
65 | const users = {
66 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
67 | '2': {id: '2', firstName: 'Foo', lastName: 'Baz'}
68 | }
69 | const payload = {
70 | '@graph': Object.values(users),
71 | '@meta': {}
72 | }
73 |
74 | spyOn(window, 'fetch').and.returnValue(
75 | Promise.resolve(new Response(JSON.stringify(payload)))
76 | )
77 |
78 | ApiHelper.getUsers()
79 | .then((res) => {
80 | expect(window.fetch).toHaveBeenCalledWith(USERS_API_URL)
81 | expect(res).toEqualImmutable(Immutable.fromJS(users))
82 | done()
83 | })
84 | })
85 | })
86 |
87 | describe('#getUsersById', () => {
88 | it('gets the user', (done) => {
89 | const users = {
90 | '1': {id: '1', firstName: 'Foo', lastName: 'Bar'},
91 | '2': {id: '2', firstName: 'Foo', lastName: 'Baz'}
92 | }
93 | const payload = {
94 | '@graph': Object.values(users),
95 | '@meta': {}
96 | }
97 |
98 | spyOn(window, 'fetch').and.returnValue(
99 | Promise.resolve(new Response(JSON.stringify(payload)))
100 | )
101 |
102 | ApiHelper.getUsersById(['1', '2'])
103 | .then((res) => {
104 | const url = template(USERS_BY_ID_API_URL)({ids: ['1', '2']})
105 | expect(window.fetch).toHaveBeenCalledWith(url)
106 | expect(res).toEqualImmutable(Immutable.fromJS(users))
107 | done()
108 | })
109 | })
110 | })
111 | })
112 |
--------------------------------------------------------------------------------
/utils/WinControlHelper/index.js:
--------------------------------------------------------------------------------
1 | export * from './src/WinControlHelper'
2 |
--------------------------------------------------------------------------------
/utils/WinControlHelper/src/WinControlHelper.js:
--------------------------------------------------------------------------------
1 | // EXPORTS /////////////////////////////////////////////////////////////////////
2 |
3 | export function getSelectedItems (winControl) {
4 | return winControl.selection.getItems().then((items) => (
5 | items.map((item) => item.data)
6 | ))
7 | }
8 |
9 | export function getFirstSelectedItem (winControl) {
10 | return winControl.selection.getItems().then((items) => (
11 | items.length > 0 ? Promise.resolve(items[0].data) : Promise.reject()
12 | ))
13 | }
14 |
15 | export function setSelection (winControl, comparator) {
16 | const indices = winControl.itemDataSource.list.reduce((memo, item, index) => {
17 | if (comparator(item)) {
18 | memo.push(index)
19 | }
20 | return memo
21 | }, [])
22 |
23 | return winControl.selection.set(indices)
24 | }
25 |
--------------------------------------------------------------------------------
/utils/WinControlHelper/test/WinControlHelper.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import React, {Component} from 'react'
3 | import ImmutablePropTypes from 'react-immutable-proptypes'
4 | import ReactWinJS from 'react-winjs'
5 | import WinJS from 'winjs'
6 |
7 | import * as WinControlHelper from 'WinControlHelper'
8 |
9 | const {TestUtils} = React.addons
10 |
11 | describe('WinControlHelper', () => {
12 | let instance
13 |
14 | beforeEach(() => {
15 | const items = Immutable.fromJS({
16 | '1': {id: '1', name: 'Foo Bar'},
17 | '2': {id: '2', name: 'Baz Qux'}
18 | })
19 |
20 | class Test extends Component {
21 | static propTypes = {
22 | items: ImmutablePropTypes.map.isRequired
23 | }
24 |
25 | componentDidMount () {
26 | const listView = this.refs.list.winControl
27 | WinControlHelper.setSelection(listView, () => true)
28 | }
29 |
30 | render () {
31 | const {items} = this.props
32 | const data = new WinJS.Binding.List(items.valueSeq().toJS())
33 |
34 | const itemRenderer = ReactWinJS.reactRenderer((item) => (
35 | {item.data.name}
36 | ))
37 |
38 | return (
39 |
44 | )
45 | }
46 | }
47 |
48 | instance = TestUtils.renderIntoDocument(
49 |
50 | )
51 | })
52 |
53 | describe('#getSelectedItems', () => {
54 | it('gets selected items', (done) => {
55 | const winControl = instance.refs.list.winControl
56 | WinControlHelper.getSelectedItems(winControl).then((selectedItems) => {
57 | expect(selectedItems.map((item) => item.id)).toEqual(['1', '2'])
58 | done()
59 | })
60 | })
61 | })
62 |
63 | describe('#getFirstSelectedItem', () => {
64 | it('gets the first selected item', (done) => {
65 | const winControl = instance.refs.list.winControl
66 | WinControlHelper.getFirstSelectedItem(winControl).then((selectedItem) => {
67 | expect(selectedItem.id).toBe('1')
68 | done()
69 | })
70 | })
71 | })
72 | })
73 |
--------------------------------------------------------------------------------