Client fixes (#8844)

* client: fix router when not authenticated, small fixes for tasks

* load the user only when necessary

* fix tests
This commit is contained in:
Matteo Pagliazzi
2017-06-29 20:49:05 +02:00
committed by GitHub
parent 06de1670b4
commit 33a39d3683
11 changed files with 131 additions and 101 deletions

View File

@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import generateStore from 'client/store'; import generateStore from 'client/store';
describe('tasks actions', () => { describe('user actions', () => {
let store; let store;
beforeEach(() => { beforeEach(() => {

View File

@@ -1,19 +1,20 @@
<!-- Entry point component for the entire app -->
<template lang="pug"> <template lang="pug">
#app #app
app-menu(v-if="userLoggedIn") router-view(v-if="!isUserLoggedIn || isStaticPage")
.container-fluid(v-if="userLoggedIn") template(v-else)
#loading-screen.h-100.w-100.d-flex.justify-content-center.align-items-center(v-if="!isUserLoaded")
p Loading...
template(v-else)
app-menu
.container-fluid
app-header app-header
router-view router-view
router-view(v-if="!userLoggedIn")
</template> </template>
<script> <script>
import AppMenu from './components/appMenu'; import AppMenu from './components/appMenu';
import AppHeader from './components/appHeader'; import AppHeader from './components/appHeader';
import axios from 'axios'; import { mapState } from 'client/libs/store';
export default { export default {
name: 'app', name: 'app',
@@ -23,44 +24,32 @@ export default {
}, },
data () { data () {
return { return {
userLoggedIn: false, isUserLoaded: false,
}; };
}, },
async beforeCreate () { computed: {
...mapState(['isUserLoggedIn']),
isStaticPage () {
return this.$route.meta.requiresLogin === false ? true : false;
},
},
created () {
// Setup listener for title // Setup listener for title
this.$store.watch(state => state.title, (title) => { this.$store.watch(state => state.title, (title) => {
document.title = title; document.title = title;
}); });
// Mount the app when user and tasks are loaded if (this.isUserLoggedIn && !this.isStaticPage) {
const userDataWatcher = this.$store.watch(state => [state.user.data, state.tasks.data], ([user, tasks]) => {
if (user && user._id && Array.isArray(tasks)) {
userDataWatcher(); // remove the watcher
// this.$mount('#app');
}
});
// @TODO: Move this to store?
let authSettings = localStorage.getItem('habit-mobile-settings');
if (!authSettings) return;
authSettings = JSON.parse(authSettings);
axios.defaults.headers.common['x-api-user'] = authSettings.auth.apiId;
axios.defaults.headers.common['x-api-key'] = authSettings.auth.apiToken;
// Load the user and the user tasks // Load the user and the user tasks
await Promise.all([ Promise.all([
this.$store.dispatch('user:fetch'), this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'), this.$store.dispatch('tasks:fetchUserTasks'),
]).catch((err) => { ]).then(() => {
console.error('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.', err); // eslint-disable-line no-console this.isUserLoaded = true;
}).catch((err) => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
}); });
}
this.userLoggedIn = true;
},
mounted () { // Remove the loading screen when the app is mounted
let loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) document.body.removeChild(loadingScreen);
}, },
}; };
</script> </script>

View File

@@ -6,51 +6,79 @@
&-worst { &-worst {
background: $maroon-100; background: $maroon-100;
&-control { &-control-habit {
background: darken($maroon-100, 12%); background: darken($maroon-100, 12%);
} }
&-control-daily-todo {
background: $maroon-500;
}
} }
&-worse { &-worse {
background: $red-100; background: $red-100;
&-control { &-control-habit {
background: darken($red-100, 12%); background: darken($red-100, 12%);
} }
&-control-daily-todo {
background: $red-500;
}
} }
&-bad { &-bad {
background: $orange-100; background: $orange-100;
&-control { &-control-habit {
background: darken($orange-100, 12%); background: darken($orange-100, 12%);
} }
&-control-daily-todo {
background: $orange-500;
}
} }
&-neutral { &-neutral {
background: $yellow-50; background: $yellow-50;
&-control { &-control-habit {
background: darken($yellow-50, 12%); background: darken($yellow-50, 12%);
} }
&-control-daily-todo {
background: $yellow-500;
}
} }
&-good { &-good {
background: $green-10; background: $green-10;
&-control { &-control-habit {
background: darken($green-10, 12%); background: darken($green-10, 12%);
} }
&-control-daily-todo {
background: $green-500;
}
} }
&-better { &-better {
background: $blue-50; background: $blue-50;
&-control { &-control-habit {
background: darken($blue-50, 12%); background: darken($blue-50, 12%);
} }
&-control-daily-todo {
background: $blue-500;
}
} }
&-best { &-best {
background: $teal-50; background: $teal-50;
&-control { &-control-habit {
background: darken($teal-50, 12%); background: darken($teal-50, 12%);
} }
&-control-daily-todo {
background: $teal-500;
}
} }
&-reward { &-reward {

View File

@@ -1,7 +1,7 @@
<template lang="pug"> <template lang="pug">
#app-header.row #app-header.row
member-details(:member="user", @click="$router.push({name: 'avatar'})") member-details(:member="user", @click="$router.push({name: 'avatar'})")
.view-party .view-party(v-if="user.party && user.party._id")
// TODO button should open the party members modal // TODO button should open the party members modal
router-link.btn.btn-primary(:active-class="''", :to="{name: 'party'}") {{ $t('viewParty') }} router-link.btn.btn-primary(:active-class="''", :to="{name: 'party'}") {{ $t('viewParty') }}
.party-members.d-flex(v-if="partyMembers && partyMembers.length > 1") .party-members.d-flex(v-if="partyMembers && partyMembers.length > 1")
@@ -62,29 +62,15 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.no-party, .party-members {
flex-grow: 1;
}
.party-members {
overflow-x: auto;
}
.no-party {
.small-text {
color: $header-color;
}
h3 { h3 {
color: $white; color: $white;
margin-bottom: 4px; margin-bottom: 4px;
} }
button { .btn {
margin-top: 16px; margin-top: 16px;
} }
} }
}
</style> </style>
<script> <script>
@@ -119,12 +105,9 @@ export default {
this.expandedMember = memberId; this.expandedMember = memberId;
} }
}, },
launchPartyModal () {
this.$root.$emit('show::modal', 'create-party-modal');
},
}, },
created () { created () {
this.getPartyMembers(); if (this.user.party && this.user.party._id) this.getPartyMembers();
}, },
}; };
</script> </script>

View File

@@ -14,7 +14,7 @@ nav.navbar.navbar-inverse.fixed-top.navbar-toggleable-sm
router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }} router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }}
router-link.nav-item(tag="li", :to="{name: 'shops'}", exact) router-link.nav-item(tag="li", :to="{name: 'shops'}", exact)
a.nav-link(v-once) {{ $t('shops') }} a.nav-link(v-once) {{ $t('shops') }}
router-link.nav-item.dropdown(:to="{name: 'party'}") router-link.nav-item(tag="li", :to="{name: 'party'}")
a.nav-link(v-once) {{ $t('party') }} a.nav-link(v-once) {{ $t('party') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}") router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}")
a.nav-link(v-once) {{ $t('guilds') }} a.nav-link(v-once) {{ $t('guilds') }}

View File

@@ -1,7 +1,7 @@
<template lang="pug"> <template lang="pug">
nav nav
a(href='/login') Login router-link(:to="{name: 'login'}") Login
a(href='/register') Register router-link(:to="{name: 'register'}") Register
</template> </template>

View File

@@ -2,11 +2,11 @@
.task.d-flex .task.d-flex
// Habits left side control // Habits left side control
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.up") .left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.up")
.task-control.habit-control(:class="controlClass.up + '-control'") .task-control.habit-control(:class="controlClass.up + '-control-habit'")
.svg-icon.positive(v-html="icons.positive") .svg-icon.positive(v-html="icons.positive")
// Dailies and todos left side control // Dailies and todos left side control
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'daily' || task.type === 'todo'", :class="controlClass") .left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'daily' || task.type === 'todo'", :class="controlClass")
.task-control.daily-todo-control(:class="controlClass + '-control'") .task-control.daily-todo-control(:class="controlClass + '-control-daily-todo'")
.svg-icon.check(v-html="icons.check", v-if="task.completed") .svg-icon.check(v-html="icons.check", v-if="task.completed")
// Task title, description and icons // Task title, description and icons
.task-content(:class="contentClass") .task-content(:class="contentClass")
@@ -39,7 +39,7 @@
// Habits right side control // Habits right side control
.right-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.down") .right-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.down")
.task-control.habit-control(:class="controlClass.down + '-control'") .task-control.habit-control(:class="controlClass.down + '-control-habit'")
.svg-icon.negative(v-html="icons.negative") .svg-icon.negative(v-html="icons.negative")
// Rewards right side control // Rewards right side control
.right-control.d-flex.align-items-center.justify-content-center.reward-control(v-if="task.type === 'reward'", :class="controlClass") .right-control.d-flex.align-items-center.justify-content-center.reward-control(v-if="task.type === 'reward'", :class="controlClass")

View File

@@ -4,14 +4,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica</title> <title>Habitica</title>
<!-- TODO load google fonts separately as @import is slow, find alternative -->
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
</head> </head>
<body> <body>
<!-- #loading-screen needs to be rendered before vue, will be deleted once app is loaded -->
<div id="loading-screen" class="h-100 w-100 d-flex justify-content-center align-items-center">
<p>Loading...</p>
</div>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>

View File

@@ -3,10 +3,9 @@
require('babel-polyfill'); require('babel-polyfill');
import Vue from 'vue'; import Vue from 'vue';
import axios from 'axios';
import AppComponent from './app'; import AppComponent from './app';
import router from './router'; import router from './router';
import generateStore from './store'; import getStore from './store';
import StoreModule from './libs/store'; import StoreModule from './libs/store';
import './filters/registerGlobals'; import './filters/registerGlobals';
import i18n from './libs/i18n'; import i18n from './libs/i18n';
@@ -26,18 +25,9 @@ Vue.config.productionTip = IS_PRODUCTION;
Vue.use(i18n); Vue.use(i18n);
Vue.use(StoreModule); Vue.use(StoreModule);
// TODO just until we have proper authentication
let authSettings = localStorage.getItem('habit-mobile-settings');
if (authSettings) {
authSettings = JSON.parse(authSettings);
axios.defaults.headers.common['x-api-user'] = authSettings.auth.apiId;
axios.defaults.headers.common['x-api-key'] = authSettings.auth.apiToken;
}
export default new Vue({ export default new Vue({
el: '#app', el: '#app',
router, router,
store: generateStore(), store: getStore(),
render: h => h(AppComponent), render: h => h(AppComponent),
}); });

View File

@@ -1,5 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import getStore from 'client/store';
import EmptyView from './components/emptyView'; import EmptyView from './components/emptyView';
@@ -38,7 +39,7 @@ const GuildPage = () => import(/* webpackChunkName: "guilds" */ './components/gu
Vue.use(VueRouter); Vue.use(VueRouter);
export default new VueRouter({ const router = new VueRouter({
mode: 'history', mode: 'history',
base: process.env.NODE_ENV === 'production' ? '/new-app' : __dirname, // eslint-disable-line no-process-env base: process.env.NODE_ENV === 'production' ? '/new-app' : __dirname, // eslint-disable-line no-process-env
linkActiveClass: 'active', linkActiveClass: 'active',
@@ -47,10 +48,11 @@ export default new VueRouter({
scrollBehavior () { scrollBehavior () {
return { x: 0, y: 0 }; return { x: 0, y: 0 };
}, },
// requiresLogin is true by default, isStatic false
routes: [ routes: [
{ name: 'home', path: '/home', component: Home }, { name: 'home', path: '/home', component: Home, meta: {requiresLogin: false} },
{ name: 'register', path: '/register', component: RegisterLogin }, { name: 'register', path: '/register', component: RegisterLogin, meta: {requiresLogin: false} },
{ name: 'login', path: '/login', component: RegisterLogin }, { name: 'login', path: '/login', component: RegisterLogin, meta: {requiresLogin: false} },
{ name: 'tasks', path: '/', component: UserTasks }, { name: 'tasks', path: '/', component: UserTasks },
{ {
path: '/inventory', path: '/inventory',
@@ -115,3 +117,22 @@ export default new VueRouter({
}, },
], ],
}); });
const store = getStore();
router.beforeEach(function routerGuard (to, from, next) {
const isUserLoggedIn = store.state.isUserLoggedIn;
const routeRequiresLogin = to.meta.requiresLogin !== false;
if (!isUserLoggedIn && routeRequiresLogin) {
// Redirect to the login page unless the user is trying to reach the
// root of the website, in which case show the home page.
// TODO when redirecting to login if user login then redirect back to initial page
// so if you tried to go to /party you'll be redirected to /party after login/signup
return next({name: to.path === '/' ? 'home' : 'login'});
}
next();
});
export default router;

View File

@@ -3,18 +3,40 @@ import deepFreeze from 'client/libs/deepFreeze';
import content from 'common/script/content/index'; import content from 'common/script/content/index';
import * as constants from 'common/script/constants'; import * as constants from 'common/script/constants';
import { asyncResourceFactory } from 'client/libs/asyncResource'; import { asyncResourceFactory } from 'client/libs/asyncResource';
import axios from 'axios';
import actions from './actions'; import actions from './actions';
import getters from './getters'; import getters from './getters';
const IS_TEST = process.env.NODE_ENV === 'test'; // eslint-disable-line no-process-env
// Load user auth parameters and determine if it's logged in
// before trying to load data
let isUserLoggedIn = false;
let AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
if (AUTH_SETTINGS) {
AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS);
axios.defaults.headers.common['x-api-user'] = AUTH_SETTINGS.auth.apiId;
axios.defaults.headers.common['x-api-key'] = AUTH_SETTINGS.auth.apiToken;
isUserLoggedIn = true;
}
// Export a function that generates the store and not the store directly // Export a function that generates the store and not the store directly
// so that we can regenerate it multiple times for testing // so that we can regenerate it multiple times for testing, when not testing
// always export the same route
let existingStore;
export default function () { export default function () {
return new Store({ if (!IS_TEST && existingStore) return existingStore;
existingStore = new Store({
actions, actions,
getters, getters,
state: { state: {
title: 'Habitica', title: 'Habitica',
isUserLoggedIn,
user: asyncResourceFactory(), user: asyncResourceFactory(),
tasks: asyncResourceFactory(), // user tasks tasks: asyncResourceFactory(), // user tasks
party: { party: {
@@ -30,4 +52,6 @@ export default function () {
constants: deepFreeze(constants), constants: deepFreeze(constants),
}, },
}); });
return existingStore;
} }