mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
Vue Store (#8071)
* vue: use our own store in place of vuex * vue store: add getters, watcher and use internal vue instance * vue store: better state getter and credits to Vuex * vue store: $watch -> watch * vuex store: pass store to getters and fix typos * add comments to store, start writing tests * fix unit tests and add missing ones * cleanup components, add less folder, fetch tassks * use Vuex helpers * pin vuex version * move semantic-ui theme to assets/less, keep website/build empty but in git * import helpers from vuex
This commit is contained in:
@@ -23,4 +23,6 @@ Gruntfile.js
|
|||||||
gulpfile.js
|
gulpfile.js
|
||||||
gulp
|
gulp
|
||||||
webpack
|
webpack
|
||||||
test/client
|
test/client/e2e
|
||||||
|
test/client/unit/index.js
|
||||||
|
test/client/unit/karma.conf.js
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,7 +14,6 @@ npm-debug.log*
|
|||||||
lib
|
lib
|
||||||
website/client-old/bower_components
|
website/client-old/bower_components
|
||||||
website/client-old/new-stuff.html
|
website/client-old/new-stuff.html
|
||||||
website/build
|
|
||||||
newrelic_agent.log
|
newrelic_agent.log
|
||||||
.bower-tmp
|
.bower-tmp
|
||||||
.bower-registry
|
.bower-registry
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import fs from 'fs';
|
|||||||
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
|
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
|
||||||
|
|
||||||
// Relative to node_modules/semantic-ui-less
|
// Relative to node_modules/semantic-ui-less
|
||||||
const SEMANTIC_THEME_PATH = '../../website/client/assets/semantic-ui/theme.config';
|
const SEMANTIC_THEME_PATH = '../../website/client/assets/less/semantic-ui/theme.config';
|
||||||
|
|
||||||
// fix well known bug with default distribution
|
// fix well known bug with default distribution
|
||||||
function fixFontPath (filename) {
|
function fixFontPath (filename) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"babel-core": "^6.0.0",
|
"babel-core": "^6.0.0",
|
||||||
"babel-loader": "^6.0.0",
|
"babel-loader": "^6.0.0",
|
||||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||||
"babel-polyfill": "^6.6.1",
|
"babel-polyfill": "^6.6.1",
|
||||||
"babel-preset-es2015": "^6.6.0",
|
"babel-preset-es2015": "^6.6.0",
|
||||||
"babel-register": "^6.6.0",
|
"babel-register": "^6.6.0",
|
||||||
@@ -114,8 +115,6 @@
|
|||||||
"vue-loader": "^9.4.0",
|
"vue-loader": "^9.4.0",
|
||||||
"vue-resource": "^1.0.2",
|
"vue-resource": "^1.0.2",
|
||||||
"vue-router": "^2.0.0-rc.5",
|
"vue-router": "^2.0.0-rc.5",
|
||||||
"vuex": "^2.0.0-rc.5",
|
|
||||||
"vuex-router-sync": "^3.0.0",
|
|
||||||
"webpack": "^1.12.2",
|
"webpack": "^1.12.2",
|
||||||
"webpack-merge": "^0.8.3",
|
"webpack-merge": "^0.8.3",
|
||||||
"winston": "^2.1.0",
|
"winston": "^2.1.0",
|
||||||
@@ -147,6 +146,7 @@
|
|||||||
"client:dev": "node webpack/dev-server.js",
|
"client:dev": "node webpack/dev-server.js",
|
||||||
"client:build": "node webpack/build.js",
|
"client:build": "node webpack/build.js",
|
||||||
"client:unit": "karma start test/client/unit/karma.conf.js --single-run",
|
"client:unit": "karma start test/client/unit/karma.conf.js --single-run",
|
||||||
|
"client:unit:watch": "karma start test/client/unit/karma.conf.js",
|
||||||
"client:e2e": "node test/client/e2e/runner.js",
|
"client:e2e": "node test/client/e2e/runner.js",
|
||||||
"client:test": "npm run client:unit && npm run client:e2e",
|
"client:test": "npm run client:unit && npm run client:e2e",
|
||||||
"start": "gulp run:dev",
|
"start": "gulp run:dev",
|
||||||
|
|||||||
5
test/client/.babelrc
Normal file
5
test/client/.babelrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"presets": ["es2015"],
|
||||||
|
"plugins": ["transform-object-rest-spread"],
|
||||||
|
"comments": false
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
// import Hello from 'src/components/Hello';
|
|
||||||
|
|
||||||
describe('Hello.vue', () => {
|
|
||||||
xit('should render correct contents', () => {
|
|
||||||
const vm = new Vue({
|
|
||||||
el: document.createElement('div'),
|
|
||||||
render: (h) => h(Hello),
|
|
||||||
});
|
|
||||||
expect(vm.$el.querySelector('.hello h1').textContent).to.equal('Hello Vue!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should make assertions', () => {
|
|
||||||
expect(true).to.equal(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
120
test/client/unit/specs/store.spec.js
Normal file
120
test/client/unit/specs/store.spec.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import storeInjector from 'inject?-vue!client/store';
|
||||||
|
import { mapState, mapGetters, mapActions } from 'client/store';
|
||||||
|
|
||||||
|
describe('Store', () => {
|
||||||
|
let injectedStore;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
injectedStore = storeInjector({ // eslint-disable-line babel/new-cap
|
||||||
|
'./state': {
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
'./getters': {
|
||||||
|
computedName ({ state }) {
|
||||||
|
return `${state.name} computed!`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'./actions': {
|
||||||
|
getName ({ state }, ...args) {
|
||||||
|
return [state.name, ...args];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).default;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects itself in all component', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
created () {
|
||||||
|
expect(this.$store).to.equal(injectedStore);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can watch a function on the state', (done) => {
|
||||||
|
injectedStore.watch(state => state.name, (newName) => {
|
||||||
|
expect(newName).to.equal('test updated');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
injectedStore.state.name = 'test updated';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports getters', () => {
|
||||||
|
expect(injectedStore.getters.computedName).to.equal('test computed!');
|
||||||
|
injectedStore.state.name = 'test updated';
|
||||||
|
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
it('can be dispatched', () => {
|
||||||
|
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error is the action doesn\'t exists', () => {
|
||||||
|
expect(() => injectedStore.dispatched('wrong')).to.throw;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('helpers', () => {
|
||||||
|
it('mapState', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
data: {
|
||||||
|
title: 'internal',
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['name']),
|
||||||
|
...mapState({
|
||||||
|
nameComputed (state, getters) {
|
||||||
|
return `${this.title} ${getters.computedName} ${state.name}`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
expect(this.name).to.equal('test');
|
||||||
|
expect(this.nameComputed).to.equal('internal test computed! test');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mapGetters', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
data: {
|
||||||
|
title: 'internal',
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['computedName']),
|
||||||
|
...mapGetters({
|
||||||
|
nameComputedTwice: 'computedName',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
expect(this.computedName).to.equal('test computed!');
|
||||||
|
expect(this.nameComputedTwice).to.equal('test computed!');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mapActions', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
data: {
|
||||||
|
title: 'internal',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['getName']),
|
||||||
|
...mapActions({
|
||||||
|
getNameRenamed: 'getName',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
expect(this.getName('123')).to.deep.equal(['test', '123']);
|
||||||
|
expect(this.getNameRenamed('123')).to.deep.equal(['test', '123']);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,7 +17,7 @@ var baseConfig = {
|
|||||||
extensions: ['', '.js', '.vue'],
|
extensions: ['', '.js', '.vue'],
|
||||||
fallback: [path.join(__dirname, '../node_modules')],
|
fallback: [path.join(__dirname, '../node_modules')],
|
||||||
alias: {
|
alias: {
|
||||||
src: path.resolve(__dirname, '../website/client'),
|
client: path.resolve(__dirname, '../website/client'),
|
||||||
assets: path.resolve(__dirname, '../website/client/assets'),
|
assets: path.resolve(__dirname, '../website/client/assets'),
|
||||||
components: path.resolve(__dirname, '../website/client/components'),
|
components: path.resolve(__dirname, '../website/client/components'),
|
||||||
},
|
},
|
||||||
|
|||||||
3
website/build/.gitignore
vendored
Normal file
3
website/build/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Ignore everything except this file so that the folder stays in git
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"presets": ["es2015"],
|
"presets": ["es2015"],
|
||||||
|
"plugins": ["transform-object-rest-spread"],
|
||||||
"comments": false
|
"comments": false
|
||||||
}
|
}
|
||||||
22
website/client/app.vue
Normal file
22
website/client/app.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!-- Entry point component for the entire app -->
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
#app.ui.fluid.container
|
||||||
|
app-header
|
||||||
|
router-view.view
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AppHeader from './components/appHeader';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AppHeader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
// Load CSS that doesn't belong to any specific component
|
||||||
|
@import './assets/less/index';
|
||||||
|
</style>
|
||||||
3
website/client/assets/less/index.less
Normal file
3
website/client/assets/less/index.less
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// CSS that doesn't belong to any specific Vue compoennt
|
||||||
|
@import './semantic-ui/semantic.less';
|
||||||
|
@import './loading-screen';
|
||||||
4
website/client/assets/less/loading-screen.less
Normal file
4
website/client/assets/less/loading-screen.less
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Rendered outside Vue
|
||||||
|
#loading-screen {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
/* Path to theme packages */
|
/* Path to theme packages */
|
||||||
@themesFolder : 'themes';
|
@themesFolder : 'themes';
|
||||||
|
|
||||||
/* Path to site override folder */
|
/* Path to site override folder - relative to node_modules/semantic-ui */
|
||||||
@siteFolder : '../../website/client/assets/semantic-ui/site';
|
@siteFolder : '../../website/client/assets/less/semantic-ui/site';
|
||||||
|
|
||||||
|
|
||||||
/*******************************
|
/*******************************
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
#app.ui.fluid.container
|
|
||||||
site-header
|
|
||||||
p Welcome back {{user.profile.name}}!
|
|
||||||
ul
|
|
||||||
li
|
|
||||||
router-link(to='/') Home
|
|
||||||
li
|
|
||||||
router-link(to='/page') Another Page
|
|
||||||
router-view.view
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import SiteHeader from './siteHeader';
|
|
||||||
import { mapState } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
SiteHeader,
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: mapState(['user']),
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
@import '../assets/semantic-ui/semantic.less';
|
|
||||||
|
|
||||||
// Element is rendered outside of Vue because it cannot wait for JS to be loaded
|
|
||||||
// Placing CSS here so it benefits from pre-processing
|
|
||||||
#loading-screen {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
h1 {{ title }}
|
#app-header
|
||||||
|
h1 {{title}}
|
||||||
|
ul
|
||||||
|
li
|
||||||
|
router-link(to='/') Home
|
||||||
|
li
|
||||||
|
router-link(to='/page') Another Page
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from '../store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: mapState([
|
computed: mapState(['title']),
|
||||||
'title',
|
|
||||||
]),
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
p {{ msg }}
|
div
|
||||||
|
p {{ msg }}
|
||||||
|
p Welcome back {{profileName}}!
|
||||||
|
p You have {{tasksCount}} tasks!
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState, mapGetters } from '../store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
msg: 'You\'re on the Home page!',
|
msg: 'You\'re on the Home page!',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
tasksCount: (state) => state.tasks.length,
|
||||||
|
}),
|
||||||
|
...mapGetters(['profileName']),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -3,15 +3,13 @@
|
|||||||
require('babel-polyfill');
|
require('babel-polyfill');
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VuexRouterSync from 'vuex-router-sync';
|
|
||||||
import VueResource from 'vue-resource';
|
import VueResource from 'vue-resource';
|
||||||
import AppComponent from './components/app';
|
import AppComponent from './app';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './vuex/store';
|
import store from './store';
|
||||||
|
|
||||||
Vue.use(VueResource);
|
|
||||||
|
|
||||||
// TODO just for the beginning
|
// TODO just for the beginning
|
||||||
|
Vue.use(VueResource);
|
||||||
|
|
||||||
let authSettings = localStorage.getItem('habit-mobile-settings');
|
let authSettings = localStorage.getItem('habit-mobile-settings');
|
||||||
|
|
||||||
@@ -21,12 +19,8 @@ if (authSettings) {
|
|||||||
Vue.http.headers.common['x-api-key'] = authSettings.auth.apiToken;
|
Vue.http.headers.common['x-api-key'] = authSettings.auth.apiToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync Vuex and Router
|
const app = new Vue({
|
||||||
VuexRouterSync.sync(store, router);
|
|
||||||
|
|
||||||
const app = new Vue({ // eslint-disable-line no-new
|
|
||||||
router,
|
router,
|
||||||
store,
|
|
||||||
render: h => h(AppComponent),
|
render: h => h(AppComponent),
|
||||||
mounted () { // Remove the loading screen when the app is mounted
|
mounted () { // Remove the loading screen when the app is mounted
|
||||||
let loadingScreen = document.getElementById('loading-screen');
|
let loadingScreen = document.getElementById('loading-screen');
|
||||||
@@ -34,21 +28,23 @@ const app = new Vue({ // eslint-disable-line no-new
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup listener for title that is outside Vue's scope
|
// Setup listener for title
|
||||||
store.watch(state => state.title, (title) => {
|
store.watch(state => state.title, (title) => {
|
||||||
document.title = title;
|
document.title = title;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mount the app when the user is loaded
|
// Mount the app when user and tasks are loaded
|
||||||
let userWatcher = store.watch(state => state.user, (user) => {
|
let userDataWatcher = store.watch(state => [state.user, state.tasks], ([user, tasks]) => {
|
||||||
if (user && user._id) {
|
if (user && user._id && tasks && tasks.length) {
|
||||||
userWatcher(); // remove the watcher
|
userDataWatcher(); // remove the watcher
|
||||||
app.$mount('#app');
|
app.$mount('#app');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load the user
|
// Load the user and the user tasks
|
||||||
store.dispatch('fetchUser')
|
Promise.all([
|
||||||
.catch(() => {
|
store.dispatch('fetchUser'),
|
||||||
alert('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.');
|
store.dispatch('fetchUserTasks'),
|
||||||
});
|
]).catch(() => {
|
||||||
|
alert('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.');
|
||||||
|
});
|
||||||
|
|||||||
25
website/client/store/actions.js
Normal file
25
website/client/store/actions.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export function setTitle (store, title) {
|
||||||
|
store.state.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUser (store) {
|
||||||
|
let promise = Vue.http.get('/api/v3/user');
|
||||||
|
|
||||||
|
promise.then((response) => {
|
||||||
|
store.state.user = response.body.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUserTasks (store) {
|
||||||
|
let promise = Vue.http.get('/api/v3/tasks/user');
|
||||||
|
|
||||||
|
promise.then((response) => {
|
||||||
|
store.state.tasks = response.body.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
15
website/client/store/getters.js
Normal file
15
website/client/store/getters.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function profileName ({ state }) {
|
||||||
|
let userProfileName = state.user.profile && state.user.profile.name;
|
||||||
|
|
||||||
|
if (!userProfileName) {
|
||||||
|
if (state.user.auth.local && state.user.auth.local.username) {
|
||||||
|
userProfileName = state.user.auth.local.username;
|
||||||
|
} else if (state.user.auth.facebook) {
|
||||||
|
userProfileName = state.user.auth.facebook.displayName || state.user.auth.facebook.username;
|
||||||
|
} else {
|
||||||
|
userProfileName = 'Anonymous';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProfileName;
|
||||||
|
}
|
||||||
67
website/client/store/helpers.js
Normal file
67
website/client/store/helpers.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/* The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-2016 Evan You
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
mapState, mapGetters and mapActions taken from Vuex v2.0.0-rc.6 as they're compatible with our
|
||||||
|
store implementation. mapMutations is not present because we do not use mutations.
|
||||||
|
|
||||||
|
Source code https://github.com/vuejs/vuex/blob/v2.0.0-rc.6/src/helpers.js
|
||||||
|
|
||||||
|
The code has been slightly changed to match our code style.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizeMap (map) {
|
||||||
|
return Array.isArray(map) ?
|
||||||
|
map.map(key => ({ key, val: key })) :
|
||||||
|
Object.keys(map).map(key => ({ key, val: map[key] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapState (states) {
|
||||||
|
const res = {};
|
||||||
|
|
||||||
|
normalizeMap(states).forEach(({ key, val }) => {
|
||||||
|
res[key] = function mappedState () {
|
||||||
|
return typeof val === 'function' ?
|
||||||
|
val.call(this, this.$store.state, this.$store.getters) :
|
||||||
|
this.$store.state[val];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapGetters (getters) {
|
||||||
|
const res = {};
|
||||||
|
|
||||||
|
normalizeMap(getters).forEach(({ key, val }) => {
|
||||||
|
res[key] = function mappedGetter () {
|
||||||
|
return this.$store.getters[val];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapActions (actions) {
|
||||||
|
const res = {};
|
||||||
|
|
||||||
|
normalizeMap(actions).forEach(({ key, val }) => {
|
||||||
|
res[key] = function mappedAction (...args) {
|
||||||
|
return this.$store.dispatch.apply(this.$store, [val].concat(args)); // eslint-disable-line prefer-spread
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
76
website/client/store/index.js
Normal file
76
website/client/store/index.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import state from './state';
|
||||||
|
import * as actions from './actions';
|
||||||
|
import * as getters from './getters';
|
||||||
|
|
||||||
|
// Central application store for Habitica
|
||||||
|
// Heavily inspired to Vuex (https://github.com/vuejs/vuex) with a very
|
||||||
|
// similar internal implementation (thanks!), main difference is the absence of mutations.
|
||||||
|
|
||||||
|
// Create a Vue instance (defined below) detatched from any DOM element to handle app data
|
||||||
|
let _vm;
|
||||||
|
|
||||||
|
// The actual store interface
|
||||||
|
const store = {
|
||||||
|
// App wide computed properties, calculated as computed properties in the internal VM
|
||||||
|
getters: {},
|
||||||
|
// Return the store's state
|
||||||
|
get state () {
|
||||||
|
return _vm.$data.state;
|
||||||
|
},
|
||||||
|
// Actions should be called using store.dispatch(ACTION_NAME, ...ARGS)
|
||||||
|
// They get passed the store instance and any additional argument passed to dispatch()
|
||||||
|
dispatch (type, ...args) {
|
||||||
|
let action = actions[type];
|
||||||
|
|
||||||
|
if (!action) throw new Error(`Action "${type}" not found.`);
|
||||||
|
return action(store, ...args);
|
||||||
|
},
|
||||||
|
// Watch data on the store's state
|
||||||
|
// Internally it uses vm.$watch and accept the same argument except
|
||||||
|
// for the first one that must be a getter function to which the state is passed
|
||||||
|
// For documentation see https://vuejs.org/api/#vm-watch
|
||||||
|
watch (getter, cb, options) {
|
||||||
|
if (typeof getter !== 'function') {
|
||||||
|
throw new Error('The first argument of store.watch must be a function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _vm.$watch(() => getter(state), cb, options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup getters
|
||||||
|
const _computed = {};
|
||||||
|
|
||||||
|
Object.keys(getters).forEach(key => {
|
||||||
|
let getter = getters[key];
|
||||||
|
|
||||||
|
// Each getter is compiled to a computed property on the internal VM
|
||||||
|
_computed[key] = () => getter(store);
|
||||||
|
|
||||||
|
Object.defineProperty(store.getters, key, {
|
||||||
|
get: () => _vm[key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup internal Vue instance to make state and getters reactive
|
||||||
|
_vm = new Vue({
|
||||||
|
data: { state },
|
||||||
|
computed: _computed,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default store;
|
||||||
|
|
||||||
|
export {
|
||||||
|
mapState,
|
||||||
|
mapGetters,
|
||||||
|
mapActions,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
|
// Inject the store into all components as this.$store
|
||||||
|
Vue.mixin({
|
||||||
|
beforeCreate () {
|
||||||
|
this.$store = store;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
7
website/client/store/state.js
Normal file
7
website/client/store/state.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const state = {
|
||||||
|
title: 'Habitica',
|
||||||
|
user: null,
|
||||||
|
tasks: null, // user tasks
|
||||||
|
};
|
||||||
|
|
||||||
|
export default state;
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
export function setTitle (store, title) {
|
|
||||||
store.commit('SET_TITLE', title);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchUser (store) {
|
|
||||||
let promise = Vue.http.get('/api/v3/user');
|
|
||||||
|
|
||||||
promise.then(response => {
|
|
||||||
store.commit('SET_USER', response.body.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export function SET_TITLE (state, title) {
|
|
||||||
state.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SET_USER (state, userJson) {
|
|
||||||
state.user = userJson;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import Vuex from 'vuex';
|
|
||||||
import * as mutations from './mutations';
|
|
||||||
import * as actions from './actions';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
title: 'Habitica',
|
|
||||||
user: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
state,
|
|
||||||
mutations,
|
|
||||||
actions,
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user