diff --git a/package.json b/package.json
index 8b439c2..8dc29e6 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"bootstrap-sass": "^3.3.7",
"color": "~1.0.3",
"cookie-parser": "~1.4.3",
+ "cors": "^2.8.3",
"crossroads": "~0.12.2",
"diff2html": "^2.3.0",
"express": "~4.15.2",
@@ -60,6 +61,7 @@
"react-dom": "^15.5.4",
"react-redux": "^5.0.4",
"redux": "^3.6.0",
+ "redux-thunk": "^2.2.0",
"rimraf": "~2.6.1",
"semver": "~5.3.0",
"serve-static": "~1.12.2",
diff --git a/source/server.js b/source/server.js
index 473151f..5f77050 100644
--- a/source/server.js
+++ b/source/server.js
@@ -3,6 +3,7 @@ const BugTracker = require('./bugtracker');
const bugtracker = new BugTracker('server');
const usageStatistics = require('./usage-statistics');
const express = require('express');
+const cors = require('cors')
const gitApi = require('./git-api');
const winston = require('winston');
const sysinfo = require('./sysinfo');
@@ -107,6 +108,7 @@ const noCache = (req, res, next) => {
app.use(noCache);
app.use(require('body-parser').json());
+app.use(cors()); // we should consider to remove it when we complete this project
if (config.autoShutdownTimeout) {
let autoShutdownTimeout;
@@ -270,6 +272,20 @@ app.get('/serverdata.js', (req, res) => {
});
});
+app.get('/ungit/config', (req, res) => {
+ sysinfo.getUserHash()
+ .then((hash) => {
+ const ungitConfig = {
+ config,
+ userHash: hash,
+ version: config.ungitDevVersion,
+ platform: os.platform(),
+ pluginApiVersion: require('../package.json').ungitPluginApiVersion
+ };
+ res.send(JSON.stringify(ungitConfig));
+ });
+});
+
app.get('/api/latestversion', (req, res) => {
sysinfo.getUngitLatestVersion()
.then((latestVersion) => {
diff --git a/src-react/actions/bootstrap.js b/src-react/actions/bootstrap.js
new file mode 100644
index 0000000..582d442
--- /dev/null
+++ b/src-react/actions/bootstrap.js
@@ -0,0 +1,12 @@
+import { fetchUngitConfig } from './ungit-config';
+import { fetchLatestVersion, fetchGitVersion } from './version';
+import { pending } from './common';
+
+export function bootstrap() {
+ return dispatch => {
+ dispatch(pending(3));
+ dispatch(fetchUngitConfig());
+ dispatch(fetchLatestVersion());
+ dispatch(fetchGitVersion());
+ };
+}
\ No newline at end of file
diff --git a/src-react/actions/common.js b/src-react/actions/common.js
new file mode 100644
index 0000000..de1a3c2
--- /dev/null
+++ b/src-react/actions/common.js
@@ -0,0 +1,16 @@
+/* This export common using actionCreator */
+import * as types from 'constants/action-types';
+
+export function pending(count) {
+ return {
+ type: types.PATH_PAGE_PENDING,
+ payload: count || 1
+ };
+};
+
+export function apiError(message) {
+ return {
+ type: types.PATH_PAGE_API_ERR,
+ payload: message
+ };
+}
\ No newline at end of file
diff --git a/src-react/actions/ungit-config.js b/src-react/actions/ungit-config.js
new file mode 100644
index 0000000..de55431
--- /dev/null
+++ b/src-react/actions/ungit-config.js
@@ -0,0 +1,29 @@
+import * as types from 'constants/action-types';
+import { fetchUserConfig } from './user-config';
+import { apiError, pending } from './common';
+
+export function fetchUngitConfig() {
+ return dispatch => {
+ // consider wrap API call in separate modules
+ // it will be easy to stub module's function when testing
+ fetch('http://localhost:8448/ungit/config')
+ .then(response => response.json())
+ .then(json => {
+ if (!json.config.bugtracking) {
+ dispatch(pending());
+ dispatch(fetchUserConfig());
+ }
+ dispatch(receiveUngitConfig(json));
+ })
+ .catch(e => {
+ dispatch(apiError(e.message));
+ });
+ };
+};
+
+function receiveUngitConfig(ungitConfig) {
+ return {
+ type: types.RECEIVE_UNGIT_CONFIG,
+ payload: ungitConfig
+ };
+};
\ No newline at end of file
diff --git a/src-react/actions/user-config.js b/src-react/actions/user-config.js
new file mode 100644
index 0000000..03f71a6
--- /dev/null
+++ b/src-react/actions/user-config.js
@@ -0,0 +1,25 @@
+import * as types from 'constants/action-types';
+import { apiError } from './common';
+
+export function fetchUserConfig() {
+ return dispatch => {
+ // consider wrap API call in separate modules
+ // it will be easy to stub module's function when testing
+ fetch('http://localhost:8448/api/userconfig')
+ .then(response => response.json())
+ .then(json => {
+ dispatch(receiveUserConfig(json));
+ })
+ .catch(e => {
+ dispatch(apiError(e.message));
+ });
+ };
+};
+
+
+function receiveUserConfig(userConfig) {
+ return {
+ type: types.RECEIVE_USER_CONFIG,
+ payload: userConfig
+ };
+};
\ No newline at end of file
diff --git a/src-react/actions/version.js b/src-react/actions/version.js
new file mode 100644
index 0000000..a4c53fb
--- /dev/null
+++ b/src-react/actions/version.js
@@ -0,0 +1,46 @@
+import * as types from 'constants/action-types';
+import { apiError } from './common';
+
+export function fetchLatestVersion() {
+ return dispatch => {
+ // consider wrap API call in separate modules
+ // it will be easy to stub module's function when testing
+ fetch('http://localhost:8448/api/latestversion')
+ .then(response => response.json())
+ .then(json => {
+ dispatch(receiveLatestVersion(json));
+ })
+ .catch(e => {
+ dispatch(apiError(e.message));
+ });
+ };
+}
+
+export function fetchGitVersion() {
+ return dispatch => {
+ // consider wrap API call in separate modules
+ // it will be easy to stub module's function when testing
+ fetch('http://localhost:8448/api/gitversion')
+ .then(response => response.json())
+ .then(json => {
+ dispatch(receiveGitVersion(json));
+ })
+ .catch(e => {
+ dispatch(apiError(e.message));
+ });
+ };
+}
+
+function receiveGitVersion(gitVersion) {
+ return {
+ type: types.RECEIVE_GIT_VERSION,
+ payload: gitVersion
+ };
+}
+
+function receiveLatestVersion(latestVersion) {
+ return {
+ type: types.RECEIVE_LATEST_VERSION,
+ payload: latestVersion
+ };
+};
\ No newline at end of file
diff --git a/src-react/components/alert-area/bug-tracking-nag-screen.js b/src-react/components/alert-area/bug-tracking-nag-screen.js
new file mode 100644
index 0000000..a587388
--- /dev/null
+++ b/src-react/components/alert-area/bug-tracking-nag-screen.js
@@ -0,0 +1,17 @@
+import React, { Component } from 'react';
+
+class BugTrackingNagScreen extends Component {
+ render() {
+ return (
+
+
×
+
Help make ungit better with the press of a button!
+
Enable automatic bug reports + anonymous usage statistics
+
Enable automatic bug reports
+
Naah, I'll skip that
+
+ );
+ }
+}
+
+export default BugTrackingNagScreen;
\ No newline at end of file
diff --git a/src-react/components/alert-area/git-version-error.js b/src-react/components/alert-area/git-version-error.js
new file mode 100644
index 0000000..e164f9a
--- /dev/null
+++ b/src-react/components/alert-area/git-version-error.js
@@ -0,0 +1,18 @@
+import React, { Component, PropTypes } from 'react';
+
+class GitVersionError extends Component {
+ static propTypes = {
+ gitVersionError: PropTypes.string
+ }
+
+ render() {
+ return (
+
+ { this.props.gitVersionError }
+ ×
+
+ );
+ }
+}
+
+export default GitVersionError;
\ No newline at end of file
diff --git a/src-react/components/alert-area/index.js b/src-react/components/alert-area/index.js
new file mode 100644
index 0000000..095d6b1
--- /dev/null
+++ b/src-react/components/alert-area/index.js
@@ -0,0 +1,62 @@
+import React, { Component, PropTypes } from 'react';
+
+import GitVersionError from './git-version-error';
+import NewVersionAvailable from './new-version-available';
+import BugTrackingNagScreen from './bug-tracking-nag-screen';
+import NPSSurvey from './nps-survey';
+
+class AlertArea extends Component {
+ static propTypes = {
+ config: PropTypes.object.isRequired,
+ gitVersionErrorVisible: PropTypes.bool.isRequired,
+ showNewVersionAvailable: PropTypes.bool.isRequired,
+ showBugtrackingNagscreen: PropTypes.bool.isRequired,
+ showNPSSurvey: PropTypes.bool.isRequired
+ }
+
+ render() {
+ const {
+ config: {
+ ungitConfig: { platform },
+ versions: {
+ gitVersion: { error },
+ latestVersion: {
+ latestVersion
+ }
+ }
+ },
+ gitVersionErrorVisible,
+ showNewVersionAvailable,
+ showBugtrackingNagscreen,
+ showNPSSurvey
+ } = this.props;
+ return (
+
+ {
+ gitVersionErrorVisible ? (
+
+ ) : null
+ }
+ {
+ showNewVersionAvailable ? (
+
+ ) : null
+ }
+ {
+ showBugtrackingNagscreen ? (
+
+ ) : null
+ }
+ {
+ showNPSSurvey ? (
+
+ ) : null
+ }
+
+ );
+ }
+}
+
+export default AlertArea;
\ No newline at end of file
diff --git a/src-react/components/alert-area/new-version-available.js b/src-react/components/alert-area/new-version-available.js
new file mode 100644
index 0000000..ed138cf
--- /dev/null
+++ b/src-react/components/alert-area/new-version-available.js
@@ -0,0 +1,24 @@
+import React, { Component, PropTypes } from 'react';
+
+class NewVersionAvailable extends Component {
+ static propTypes = {
+ latestVersion: PropTypes.string,
+ platform: PropTypes.string
+ }
+
+ render() {
+ const newVersionInstallCommand = `#{this.props.platform ? '' : 'sudo -H'}npm update -g ungit`;
+ return (
+
+ A new version of ungit (
{ this.props.latestVersion } ) is
+
available ! Run
+
{ newVersionInstallCommand }
to install.
+ See what's new in the
+
changelog .
+
×
+
+ );
+ }
+}
+
+export default NewVersionAvailable;
\ No newline at end of file
diff --git a/src-react/components/alert-area/nps-survey.js b/src-react/components/alert-area/nps-survey.js
new file mode 100644
index 0000000..b313d9c
--- /dev/null
+++ b/src-react/components/alert-area/nps-survey.js
@@ -0,0 +1,37 @@
+import React, { Component } from 'react';
+
+class NPSSurvey extends Component {
+ static propTypes = {
+ }
+
+ render() {
+ return (
+
+
×
+
Hi! This is a one-question survey to learn more about how people use Ungit. You can dismiss it by clicking the x in the upper right corner.
+
Question: How likely are you to recommend Ungit to your friends and colleagues?
+
+
+
+ Not at all likely
+
Extremely likely
+
+
+
+ );
+ }
+}
+
+export default NPSSurvey;
\ No newline at end of file
diff --git a/src-react/constants/action-types.js b/src-react/constants/action-types.js
new file mode 100644
index 0000000..69ec0e4
--- /dev/null
+++ b/src-react/constants/action-types.js
@@ -0,0 +1,6 @@
+export const RECEIVE_GIT_VERSION = 'RECEIVE_GIT_VERSION';
+export const RECEIVE_LATEST_VERSION = 'RECEIVE_LATEST_VERSION';
+export const RECEIVE_USER_CONFIG = 'RECEIVE_USER_CONFIG';
+export const RECEIVE_UNGIT_CONFIG = 'RECEIVE_UNGIT_CONFIG';
+export const PATH_PAGE_PENDING = 'PATH_PAGE_PENDING';
+export const PATH_PAGE_API_ERR = 'PATH_PAGE_API_ERR';
\ No newline at end of file
diff --git a/src-react/containers/path.js b/src-react/containers/path.js
index 499d4e9..2585882 100644
--- a/src-react/containers/path.js
+++ b/src-react/containers/path.js
@@ -1,19 +1,41 @@
import React, { Component } from 'react';
+import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'
+import AlertArea from 'components/alert-area';
+import * as bootstrapActionCreators from 'actions/bootstrap';
import 'styles/styles.scss';
-@connect(state => { return { ...state } })
+@connect(state => {
+ return { ...state };
+}, dispatch => {
+ return {
+ actions: bindActionCreators(Object.assign({}, bootstrapActionCreators), dispatch)
+ };
+})
class Path extends Component {
+
+ componentWillMount() {
+ const { actions } = this.props;
+ actions.bootstrap();
+ }
+
render() {
+ const { path: { pending, errMessage }, app } = this.props;
return (
-
-
-
Welcome to { this.props.app }
+
+
+
+ {
+ pending === 0 && errMessage.length === 0 ? (
+
+ ) : null
+ }
-
- To get started, edit src/container/Path.js
and save to reload.
-
);
}
diff --git a/src-react/reducers/app.js b/src-react/reducers/app.js
new file mode 100644
index 0000000..52c889e
--- /dev/null
+++ b/src-react/reducers/app.js
@@ -0,0 +1,48 @@
+import * as types from 'constants/action-types';
+
+function app(state, action, config) {
+
+ switch(action.type) {
+ case types.RECEIVE_UNGIT_CONFIG: {
+ const { ungitConfig } = config;
+ const bugtrackingNagscreenDismissed = localStorage.getItem('bugtrackingNagscreenDismissed');
+ const showBugtrackingNagscreen = !ungitConfig.config.bugtracking && !bugtrackingNagscreenDismissed;
+
+ return { ...state, showBugtrackingNagscreen };
+ }
+
+ case types.RECEIVE_GIT_VERSION: {
+ const { ungitConfig, versions: { gitVersion } } = config;
+ const gitVersionCheckOverride = ungitConfig.config && ungitConfig.config.gitVersionCheckOverride;
+ const gitVersionErrorDismissed = localStorage.getItem('gitVersionErrorDismissed');
+ const gitVersionError = gitVersion && !gitVersion.satisfied && gitVersion.error;
+ const gitVersionErrorVisible = !gitVersionCheckOverride && gitVersionError && gitVersionErrorDismissed;
+
+ return { ...state, gitVersionErrorVisible };
+ }
+
+ case types.RECEIVE_LATEST_VERSION: {
+ const { ungitConfig, versions: { latestVersion } } = config;
+ const gitVersionCheckOverride = ungitConfig.config && ungitConfig.config.gitVersionCheckOverride;
+ const outdated = latestVersion && latestVersion.outdated;
+ const showNewVersionAvailable = !gitVersionCheckOverride && outdated;
+
+ return { ...state, showNewVersionAvailable };
+ }
+
+ case types.RECEIVE_USER_CONFIG: {
+ const { userConfig } = config;
+ const bugtrackingNagscreenDismissed = localStorage.getItem('bugtrackingNagscreenDismissed');
+ const showBugtrackingNagscreen = !userConfig.bugtracking && !bugtrackingNagscreenDismissed;
+
+ return { ...state, showBugtrackingNagscreen };
+ }
+ default:
+ const NPSSurveyLastDismissed = parseInt(localStorage.getItem('NPSSurveyLastDismissed') || '0', 10);
+ const monthsSinceNPSLastDismissed = (Date.now() - NPSSurveyLastDismissed) / (1000 * 60 * 60 * 24 * 30);
+ const showNPSSurvey = monthsSinceNPSLastDismissed >= 6 && Math.random() < 0.01;
+ return { ...state, showNPSSurvey };
+ }
+}
+
+export default app;
\ No newline at end of file
diff --git a/src-react/reducers/config.js b/src-react/reducers/config.js
new file mode 100644
index 0000000..fcb2a08
--- /dev/null
+++ b/src-react/reducers/config.js
@@ -0,0 +1,13 @@
+import { combineReducers } from 'redux';
+
+import ungitConfig from './ungit-config';
+import userConfig from './user-config';
+import versions from './versions';
+
+const config = combineReducers({
+ ungitConfig,
+ userConfig,
+ versions
+});
+
+export default config;
\ No newline at end of file
diff --git a/src-react/reducers/index.js b/src-react/reducers/index.js
index c89fed9..2659960 100644
--- a/src-react/reducers/index.js
+++ b/src-react/reducers/index.js
@@ -1,9 +1,14 @@
-const initialState = {
- app: 'React'
-};
+import config from './config';
+import app from './app';
+import path from './path';
-const ungitApp = function(state, action) {
- return { ...initialState };
+function ungitApp(state, action) {
+ const _config = config(state.config, action);
+ return {
+ config: _config,
+ app: app(state.app, action, _config),
+ path: path(state.path, action, _config),
+ };
}
export default ungitApp;
\ No newline at end of file
diff --git a/src-react/reducers/path.js b/src-react/reducers/path.js
new file mode 100644
index 0000000..f3d3469
--- /dev/null
+++ b/src-react/reducers/path.js
@@ -0,0 +1,20 @@
+import * as types from 'constants/action-types';
+
+function path(state, action) {
+ switch(action.type) {
+ case types.PATH_PAGE_PENDING:
+ return { ...state, pending: state.pending + action.payload };
+ case types.RECEIVE_UNGIT_CONFIG:
+ case types.RECEIVE_USER_CONFIG:
+ case types.RECEIVE_GIT_VERSION:
+ case types.RECEIVE_LATEST_VERSION:
+ return { ...state, pending: state.pending - 1 };
+ case types.PATH_PAGE_API_ERR:
+ const { payload: errMessage } = action;
+ return { ...state, pending: state.pending - 1, errMessage: [ ...state.errMessage, errMessage ] };
+ default:
+ return { ...state };
+ }
+}
+
+export default path;
\ No newline at end of file
diff --git a/src-react/reducers/ungit-config.js b/src-react/reducers/ungit-config.js
new file mode 100644
index 0000000..6bbac31
--- /dev/null
+++ b/src-react/reducers/ungit-config.js
@@ -0,0 +1,13 @@
+import * as types from 'constants/action-types';
+
+function ungitConfig(state, action) {
+ switch(action.type) {
+ case types.RECEIVE_UNGIT_CONFIG:
+ const { payload: ungitConfig } = action;
+ return { ...ungitConfig };
+ default:
+ return { ...state };
+ }
+}
+
+export default ungitConfig;
\ No newline at end of file
diff --git a/src-react/reducers/user-config.js b/src-react/reducers/user-config.js
new file mode 100644
index 0000000..23f3b65
--- /dev/null
+++ b/src-react/reducers/user-config.js
@@ -0,0 +1,13 @@
+import * as types from 'constants/action-types';
+
+function userConfig(state, action) {
+ switch(action.type) {
+ case types.RECEIVE_USER_CONFIG:
+ const { payload: userConfig } = action;
+ return { ...userConfig };
+ default:
+ return { ...state };
+ }
+}
+
+export default userConfig;
\ No newline at end of file
diff --git a/src-react/reducers/versions.js b/src-react/reducers/versions.js
new file mode 100644
index 0000000..9ddae17
--- /dev/null
+++ b/src-react/reducers/versions.js
@@ -0,0 +1,16 @@
+import * as types from 'constants/action-types';
+
+function userConfig(state, action) {
+ switch(action.type) {
+ case types.RECEIVE_GIT_VERSION:
+ const { payload: gitVersion } = action;
+ return { ...state, gitVersion };
+ case types.RECEIVE_LATEST_VERSION:
+ const { payload: latestVersion } = action;
+ return { ...state, latestVersion };
+ default:
+ return { ...state };
+ }
+}
+
+export default userConfig;
\ No newline at end of file
diff --git a/src-react/store.js b/src-react/store.js
index 5af56c6..88a8839 100644
--- a/src-react/store.js
+++ b/src-react/store.js
@@ -1,6 +1,28 @@
-import { createStore } from 'redux';
+import { createStore, applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
import ungitApp from './reducers';
-const store = createStore(ungitApp);
+const initialState = {
+ config: {
+ ungitConfig: null,
+ userConfig: null,
+ versions: {
+ gitVersion: null,
+ latestVersion: null
+ }
+ },
+ app: {
+ gitVersionErrorVisible: false,
+ showNewVersionAvailable: false,
+ showBugtrackingNagscreen: false,
+ showNPSSurvey: false
+ },
+ path: {
+ pending: null,
+ errMessage: []
+ }
+};
+
+const store = createStore(ungitApp, initialState, applyMiddleware(thunk));
export default store;
\ No newline at end of file