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 generateStore from 'client/store';
describe('tasks actions', () => {
describe('user actions', () => {
let store;
beforeEach(() => {

View File

@@ -1,19 +1,20 @@
<!-- Entry point component for the entire app -->
<template lang="pug">
#app
app-menu(v-if="userLoggedIn")
.container-fluid(v-if="userLoggedIn")
router-view(v-if="!isUserLoggedIn || isStaticPage")
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
router-view
router-view(v-if="!userLoggedIn")
</template>
<script>
import AppMenu from './components/appMenu';
import AppHeader from './components/appHeader';
import axios from 'axios';
import { mapState } from 'client/libs/store';
export default {
name: 'app',
@@ -23,44 +24,32 @@ export default {
},
data () {
return {
userLoggedIn: false,
isUserLoaded: false,
};
},
async beforeCreate () {
computed: {
...mapState(['isUserLoggedIn']),
isStaticPage () {
return this.$route.meta.requiresLogin === false ? true : false;
},
},
created () {
// Setup listener for title
this.$store.watch(state => state.title, (title) => {
document.title = title;
});
// Mount the app when user and tasks are loaded
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;
if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks
await Promise.all([
Promise.all([
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).catch((err) => {
console.error('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.', err); // eslint-disable-line no-console
]).then(() => {
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>

View File

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

View File

@@ -1,7 +1,7 @@
<template lang="pug">
#app-header.row
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
router-link.btn.btn-primary(:active-class="''", :to="{name: 'party'}") {{ $t('viewParty') }}
.party-members.d-flex(v-if="partyMembers && partyMembers.length > 1")
@@ -62,29 +62,15 @@
flex-wrap: nowrap;
}
.no-party, .party-members {
flex-grow: 1;
}
.party-members {
overflow-x: auto;
}
.no-party {
.small-text {
color: $header-color;
}
h3 {
color: $white;
margin-bottom: 4px;
}
button {
.btn {
margin-top: 16px;
}
}
}
</style>
<script>
@@ -119,12 +105,9 @@ export default {
this.expandedMember = memberId;
}
},
launchPartyModal () {
this.$root.$emit('show::modal', 'create-party-modal');
},
},
created () {
this.getPartyMembers();
if (this.user.party && this.user.party._id) this.getPartyMembers();
},
};
</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.nav-item(tag="li", :to="{name: 'shops'}", exact)
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') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}")
a.nav-link(v-once) {{ $t('guilds') }}

View File

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

View File

@@ -2,11 +2,11 @@
.task.d-flex
// Habits left side control
.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")
// 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")
.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")
// Task title, description and icons
.task-content(:class="contentClass")
@@ -39,7 +39,7 @@
// Habits right side control
.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")
// Rewards right side control
.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 name="viewport" content="width=device-width, initial-scale=1">
<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">
</head>
<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>
<!-- built files will be auto injected -->
</body>

View File

@@ -3,10 +3,9 @@
require('babel-polyfill');
import Vue from 'vue';
import axios from 'axios';
import AppComponent from './app';
import router from './router';
import generateStore from './store';
import getStore from './store';
import StoreModule from './libs/store';
import './filters/registerGlobals';
import i18n from './libs/i18n';
@@ -26,18 +25,9 @@ Vue.config.productionTip = IS_PRODUCTION;
Vue.use(i18n);
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({
el: '#app',
router,
store: generateStore(),
store: getStore(),
render: h => h(AppComponent),
});

View File

@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import getStore from 'client/store';
import EmptyView from './components/emptyView';
@@ -38,7 +39,7 @@ const GuildPage = () => import(/* webpackChunkName: "guilds" */ './components/gu
Vue.use(VueRouter);
export default new VueRouter({
const router = new VueRouter({
mode: 'history',
base: process.env.NODE_ENV === 'production' ? '/new-app' : __dirname, // eslint-disable-line no-process-env
linkActiveClass: 'active',
@@ -47,10 +48,11 @@ export default new VueRouter({
scrollBehavior () {
return { x: 0, y: 0 };
},
// requiresLogin is true by default, isStatic false
routes: [
{ name: 'home', path: '/home', component: Home },
{ name: 'register', path: '/register', component: RegisterLogin },
{ name: 'login', path: '/login', component: RegisterLogin },
{ name: 'home', path: '/home', component: Home, meta: {requiresLogin: false} },
{ name: 'register', path: '/register', component: RegisterLogin, meta: {requiresLogin: false} },
{ name: 'login', path: '/login', component: RegisterLogin, meta: {requiresLogin: false} },
{ name: 'tasks', path: '/', component: UserTasks },
{
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 * as constants from 'common/script/constants';
import { asyncResourceFactory } from 'client/libs/asyncResource';
import axios from 'axios';
import actions from './actions';
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
// 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 () {
return new Store({
if (!IS_TEST && existingStore) return existingStore;
existingStore = new Store({
actions,
getters,
state: {
title: 'Habitica',
isUserLoggedIn,
user: asyncResourceFactory(),
tasks: asyncResourceFactory(), // user tasks
party: {
@@ -30,4 +52,6 @@ export default function () {
constants: deepFreeze(constants),
},
});
return existingStore;
}