mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 06:37:23 +01:00
Client: async resources, make store reusable, move plugins and add getTaskFor getter (#8575)
Add library to manage async resource Make Store reusable for easier testing Move plugin to libs Add getTaskFor getter with tests
This commit is contained in:
151
test/client/unit/specs/libs/asyncResource.js
Normal file
151
test/client/unit/specs/libs/asyncResource.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { asyncResourceFactory, loadAsyncResource } from 'client/libs/asyncResource';
|
||||
import axios from 'axios';
|
||||
import generateStore from 'client/store';
|
||||
import { sleep } from '../../../../helpers/sleep';
|
||||
|
||||
describe('async resource', () => {
|
||||
it('asyncResourceFactory', () => {
|
||||
const resource = asyncResourceFactory();
|
||||
expect(resource.loadingStatus).to.equal('NOT_LOADED');
|
||||
expect(resource.data).to.equal(null);
|
||||
expect(resource).to.not.equal(asyncResourceFactory);
|
||||
});
|
||||
|
||||
describe('loadAsyncResource', () => {
|
||||
context('errors', () => {
|
||||
it('store is missing', () => {
|
||||
expect(() => loadAsyncResource({})).to.throw;
|
||||
});
|
||||
it('path is missing', () => {
|
||||
expect(() => loadAsyncResource({
|
||||
store: 'store',
|
||||
})).to.throw;
|
||||
});
|
||||
it('url is missing', () => {
|
||||
expect(() => loadAsyncResource({
|
||||
store: 'store',
|
||||
path: 'path',
|
||||
})).to.throw;
|
||||
});
|
||||
it('deserialize is missing', () => {
|
||||
expect(() => loadAsyncResource({
|
||||
store: 'store',
|
||||
path: 'path',
|
||||
url: 'url',
|
||||
})).to.throw;
|
||||
});
|
||||
it('resource not found', () => {
|
||||
const store = generateStore();
|
||||
|
||||
expect(() => loadAsyncResource({
|
||||
store,
|
||||
path: 'not existing path',
|
||||
url: 'url',
|
||||
deserialize: 'deserialize',
|
||||
})).to.throw;
|
||||
});
|
||||
|
||||
it('invalid loading status', () => {
|
||||
const store = generateStore();
|
||||
store.state.user.loadingStatus = 'INVALID';
|
||||
|
||||
expect(loadAsyncResource({
|
||||
store,
|
||||
path: 'user',
|
||||
url: 'url',
|
||||
deserialize: 'deserialize',
|
||||
})).to.eventually.be.rejected;
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the resource if it is already loaded and forceLoad is false', async () => {
|
||||
const store = generateStore();
|
||||
store.state.user.loadingStatus = 'LOADED';
|
||||
store.state.user.data = {_id: 1};
|
||||
|
||||
sandbox.stub(axios, 'get');
|
||||
|
||||
const resource = await loadAsyncResource({
|
||||
store,
|
||||
path: 'user',
|
||||
url: 'url',
|
||||
deserialize: 'deserialize',
|
||||
});
|
||||
|
||||
expect(resource).to.equal(store.state.user);
|
||||
expect(axios.get).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('load the resource if it is not loaded', async () => {
|
||||
const store = generateStore();
|
||||
store.state.user = asyncResourceFactory();
|
||||
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
|
||||
|
||||
const resource = await loadAsyncResource({
|
||||
store,
|
||||
path: 'user',
|
||||
url: '/api/v3/user',
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
expect(resource).to.equal(store.state.user);
|
||||
expect(resource.loadingStatus).to.equal('LOADED');
|
||||
expect(resource.data._id).to.equal(1);
|
||||
expect(axios.get).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('load the resource if it is loaded but forceLoad is true', async () => {
|
||||
const store = generateStore();
|
||||
store.state.user.loadingStatus = 'LOADED';
|
||||
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
|
||||
|
||||
const resource = await loadAsyncResource({
|
||||
store,
|
||||
path: 'user',
|
||||
url: '/api/v3/user',
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
forceLoad: true,
|
||||
});
|
||||
|
||||
expect(resource).to.equal(store.state.user);
|
||||
expect(resource.loadingStatus).to.equal('LOADED');
|
||||
expect(resource.data._id).to.equal(1);
|
||||
expect(axios.get).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('does not send multiple requests if the resource is being loaded', async () => {
|
||||
const store = generateStore();
|
||||
store.state.user.loadingStatus = 'LOADING';
|
||||
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
|
||||
|
||||
const resourcePromise = loadAsyncResource({
|
||||
store,
|
||||
path: 'user',
|
||||
url: '/api/v3/user',
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
forceLoad: true,
|
||||
});
|
||||
|
||||
await sleep(0.1);
|
||||
const userData = {_id: 1};
|
||||
|
||||
expect(store.state.user.loadingStatus).to.equal('LOADING');
|
||||
expect(axios.get).to.not.have.been.called;
|
||||
store.state.user.data = userData;
|
||||
store.state.user.loadingStatus = 'LOADED';
|
||||
|
||||
const result = await resourcePromise;
|
||||
expect(axios.get).to.not.have.been.called;
|
||||
expect(result).to.equal(store.state.user);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import deepFreeze from 'client/libs/deepFreeze';
|
||||
|
||||
describe('deepFreeze', () => {
|
||||
it('works as expected', () => {
|
||||
it('deeply freezes an object', () => {
|
||||
let obj = {
|
||||
a: 1,
|
||||
b () {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import i18n from 'client/plugins/i18n';
|
||||
import i18n from 'client/libs/i18n';
|
||||
import commoni18n from 'common/script/i18n';
|
||||
import Vue from 'vue';
|
||||
|
||||
describe('i18n plugin', () => {
|
||||
before(() => {
|
||||
i18n.install(Vue);
|
||||
Vue.use(i18n);
|
||||
});
|
||||
|
||||
it('adds $t to Vue.prototype', () => {
|
||||
178
test/client/unit/specs/libs/store.js
Normal file
178
test/client/unit/specs/libs/store.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import Vue from 'vue';
|
||||
import StoreModule, { mapState, mapGetters, mapActions } from 'client/libs/store';
|
||||
import { flattenAndNamespace } from 'client/libs/store/helpers/internals';
|
||||
|
||||
describe('Store', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new StoreModule({ // eslint-disable-line babel/new-cap
|
||||
state: {
|
||||
name: 'test',
|
||||
nested: {
|
||||
name: 'nested state test',
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
computedName ({ state }) {
|
||||
return `${state.name} computed!`;
|
||||
},
|
||||
...flattenAndNamespace({
|
||||
nested: {
|
||||
computedName ({ state }) {
|
||||
return `${state.name} computed!`;
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
actions: {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
...flattenAndNamespace({
|
||||
nested: {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
Vue.use(StoreModule);
|
||||
});
|
||||
|
||||
it('injects itself in all component', (done) => {
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
store,
|
||||
created () {
|
||||
expect(this.$store).to.equal(store);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can watch a function on the state', (done) => {
|
||||
store.watch(state => state.name, (newName) => {
|
||||
expect(newName).to.equal('test updated');
|
||||
done();
|
||||
});
|
||||
|
||||
store.state.name = 'test updated';
|
||||
});
|
||||
|
||||
describe('getters', () => {
|
||||
it('supports getters', () => {
|
||||
expect(store.getters.computedName).to.equal('test computed!');
|
||||
store.state.name = 'test updated';
|
||||
expect(store.getters.computedName).to.equal('test updated computed!');
|
||||
});
|
||||
|
||||
it('supports nested getters', () => {
|
||||
expect(store.getters['nested:computedName']).to.equal('test computed!');
|
||||
store.state.name = 'test updated';
|
||||
expect(store.getters['nested:computedName']).to.equal('test updated computed!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('can dispatch an action', () => {
|
||||
expect(store.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('can dispatch a nested action', () => {
|
||||
expect(store.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('throws an error is the action doesn\'t exists', () => {
|
||||
expect(() => store.dispatched('wrong')).to.throw;
|
||||
});
|
||||
});
|
||||
|
||||
describe('helpers', () => {
|
||||
it('mapState', (done) => {
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
store,
|
||||
data: {
|
||||
title: 'internal',
|
||||
},
|
||||
computed: {
|
||||
...mapState(['name']),
|
||||
...mapState({
|
||||
nameComputed (state, getters) {
|
||||
return `${this.title} ${getters.computedName} ${state.name}`;
|
||||
},
|
||||
}),
|
||||
...mapState({nestedTest: 'nested.name'}),
|
||||
},
|
||||
created () {
|
||||
expect(this.name).to.equal('test');
|
||||
expect(this.nameComputed).to.equal('internal test computed! test');
|
||||
expect(this.nestedTest).to.equal('nested state test');
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('mapGetters', (done) => {
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
store,
|
||||
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
|
||||
store,
|
||||
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();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('flattenAndNamespace', () => {
|
||||
let result = flattenAndNamespace({
|
||||
nested: {
|
||||
computed ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
nested2: {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Object.keys(result).length).to.equal(3);
|
||||
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { fetchAll as fetchAllGuilds } from 'client/store/actions/guilds';
|
||||
import axios from 'axios';
|
||||
import store from 'client/store';
|
||||
|
||||
describe('guilds actions', () => {
|
||||
it('fetchAll', async () => {
|
||||
const guilds = [{_id: 1}];
|
||||
sandbox
|
||||
.stub(axios, 'get')
|
||||
.withArgs('/api/v3/groups?type=publicGuilds')
|
||||
.returns(Promise.resolve({data: {data: guilds}}));
|
||||
|
||||
await fetchAllGuilds(store);
|
||||
|
||||
expect(store.state.guilds).to.equal(guilds);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,54 @@
|
||||
import { fetchUserTasks } from 'client/store/actions/tasks';
|
||||
import axios from 'axios';
|
||||
import store from 'client/store';
|
||||
import generateStore from 'client/store';
|
||||
|
||||
describe('tasks actions', () => {
|
||||
it('fetchUserTasks', async () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = generateStore();
|
||||
});
|
||||
|
||||
describe('fetchUserTasks', () => {
|
||||
it('fetches user tasks', async () => {
|
||||
expect(store.state.tasks.loadingStatus).to.equal('NOT_LOADED');
|
||||
const tasks = [{_id: 1}];
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
||||
|
||||
await fetchUserTasks(store);
|
||||
await store.dispatch('tasks:fetchUserTasks');
|
||||
|
||||
expect(store.state.tasks).to.equal(tasks);
|
||||
expect(store.state.tasks.data).to.equal(tasks);
|
||||
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
|
||||
});
|
||||
|
||||
it('does not reload tasks by default', async () => {
|
||||
const originalTask = [{_id: 1}];
|
||||
store.state.tasks = {
|
||||
loadingStatus: 'LOADED',
|
||||
data: originalTask,
|
||||
};
|
||||
|
||||
const tasks = [{_id: 2}];
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
||||
|
||||
await store.dispatch('tasks:fetchUserTasks');
|
||||
|
||||
expect(store.state.tasks.data).to.equal(originalTask);
|
||||
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
|
||||
});
|
||||
|
||||
it('can reload tasks if forceLoad is true', async () => {
|
||||
store.state.tasks = {
|
||||
loadingStatus: 'LOADED',
|
||||
data: [{_id: 1}],
|
||||
};
|
||||
|
||||
const tasks = [{_id: 2}];
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
||||
|
||||
await store.dispatch('tasks:fetchUserTasks', true);
|
||||
|
||||
expect(store.state.tasks.data).to.equal(tasks);
|
||||
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,54 @@
|
||||
import { fetch as fetchUser } from 'client/store/actions/user';
|
||||
import axios from 'axios';
|
||||
import store from 'client/store';
|
||||
import generateStore from 'client/store';
|
||||
|
||||
describe('user actions', () => {
|
||||
it('fetch', async () => {
|
||||
describe('tasks actions', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = generateStore();
|
||||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
it('loads the user', async () => {
|
||||
expect(store.state.user.loadingStatus).to.equal('NOT_LOADED');
|
||||
const user = {_id: 1};
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
|
||||
|
||||
await fetchUser(store);
|
||||
await store.dispatch('user:fetch');
|
||||
|
||||
expect(store.state.user).to.equal(user);
|
||||
expect(store.state.user.data).to.equal(user);
|
||||
expect(store.state.user.loadingStatus).to.equal('LOADED');
|
||||
});
|
||||
|
||||
it('does not reload user by default', async () => {
|
||||
const originalUser = {_id: 1};
|
||||
store.state.user = {
|
||||
loadingStatus: 'LOADED',
|
||||
data: originalUser,
|
||||
};
|
||||
|
||||
const user = {_id: 2};
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
|
||||
|
||||
await store.dispatch('user:fetch');
|
||||
|
||||
expect(store.state.user.data).to.equal(originalUser);
|
||||
expect(store.state.user.loadingStatus).to.equal('LOADED');
|
||||
});
|
||||
|
||||
it('can reload user if forceLoad is true', async () => {
|
||||
store.state.user = {
|
||||
loadingStatus: 'LOADED',
|
||||
data: {_id: 1},
|
||||
};
|
||||
|
||||
const user = {_id: 2};
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
|
||||
|
||||
await store.dispatch('user:fetch', true);
|
||||
|
||||
expect(store.state.user.data).to.equal(user);
|
||||
expect(store.state.user.loadingStatus).to.equal('LOADED');
|
||||
});
|
||||
});
|
||||
});
|
||||
16
test/client/unit/specs/store/getters/tasks/getTagsFor.js
Normal file
16
test/client/unit/specs/store/getters/tasks/getTagsFor.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import generateStore from 'client/store';
|
||||
|
||||
describe('getTagsFor getter', () => {
|
||||
it('returns the tags for a task', () => {
|
||||
const store = generateStore();
|
||||
store.state.user.data = {
|
||||
tags: [
|
||||
{id: 1, name: 'tag 1'},
|
||||
{id: 2, name: 'tag 2'},
|
||||
],
|
||||
};
|
||||
|
||||
const task = {tags: [2]};
|
||||
expect(store.getters['tasks:getTagsFor'](task)).to.deep.equal(['tag 2']);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ describe('userGems getter', () => {
|
||||
expect(userGems({
|
||||
state: {
|
||||
user: {
|
||||
balance: 4.5,
|
||||
data: {balance: 4.5},
|
||||
},
|
||||
},
|
||||
})).to.equal(18);
|
||||
@@ -1,168 +1,8 @@
|
||||
import Vue from 'vue';
|
||||
import storeInjector from 'inject-loader?-vue!client/store';
|
||||
import { mapState, mapGetters, mapActions } from 'client/store';
|
||||
import { flattenAndNamespace } from 'client/store/helpers/internals';
|
||||
import generateStore from 'client/store';
|
||||
import Store from 'client/libs/store';
|
||||
|
||||
describe('Store', () => {
|
||||
let injectedStore;
|
||||
|
||||
beforeEach(() => {
|
||||
injectedStore = storeInjector({ // eslint-disable-line babel/new-cap
|
||||
'./state': {
|
||||
name: 'test',
|
||||
},
|
||||
'./getters': {
|
||||
computedName ({ state }) {
|
||||
return `${state.name} computed!`;
|
||||
},
|
||||
...flattenAndNamespace({
|
||||
nested: {
|
||||
computedName ({ state }) {
|
||||
return `${state.name} computed!`;
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
'./actions': {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
...flattenAndNamespace({
|
||||
nested: {
|
||||
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';
|
||||
});
|
||||
|
||||
describe('getters', () => {
|
||||
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!');
|
||||
});
|
||||
|
||||
it('supports nested getters', () => {
|
||||
expect(injectedStore.getters['nested:computedName']).to.equal('test computed!');
|
||||
injectedStore.state.name = 'test updated';
|
||||
expect(injectedStore.getters['nested:computedName']).to.equal('test updated computed!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('can dispatch an action', () => {
|
||||
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('can dispatch a nested action', () => {
|
||||
expect(injectedStore.dispatch('nested: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();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('flattenAndNamespace', () => {
|
||||
let result = flattenAndNamespace({
|
||||
nested: {
|
||||
computed ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
nested2: {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Object.keys(result).length).to.equal(3);
|
||||
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
|
||||
});
|
||||
describe('Application store', () => {
|
||||
it('is an instance of Store', () => {
|
||||
expect(generateStore()).to.be.an.instanceof(Store);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
#app-header.row
|
||||
avatar#header-avatar(:user="$store.state.user")
|
||||
avatar#header-avatar(:user="user")
|
||||
div
|
||||
span.character-name {{user.profile.name}}
|
||||
span.character-level Lvl {{user.stats.lvl}}
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
<script>
|
||||
import Avatar from './avatar';
|
||||
import { mapState } from '../store';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import { toNextLevel } from '../../common/script/statHelpers';
|
||||
import { MAX_HEALTH as maxHealth } from '../../common/script/constants';
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
...mapState({user: 'user.data'}),
|
||||
maxMP () {
|
||||
return statsComputed(this.user).maxMP;
|
||||
},
|
||||
|
||||
@@ -130,14 +130,14 @@ $active-purple: #6133b4;
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from '../store';
|
||||
import { mapState, mapGetters } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
userGems: 'user:gems',
|
||||
}),
|
||||
...mapState(['user']),
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -8,7 +8,3 @@
|
||||
.col-12
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
@@ -73,7 +73,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '../../store';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import each from 'lodash/each';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -4,7 +4,3 @@
|
||||
h2 Page
|
||||
p {{ $route.path }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default { };
|
||||
</script>
|
||||
@@ -3,7 +3,3 @@
|
||||
.col
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default { };
|
||||
</script>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import MugenScroll from 'vue-mugen-scroll';
|
||||
import { mapState } from 'client/store';
|
||||
import PublicGuildItem from './publicGuildItem';
|
||||
import { GUILDS_PER_PAGE } from 'common/script/constants';
|
||||
|
||||
@@ -51,9 +50,6 @@ export default {
|
||||
guilds: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['guilds']),
|
||||
},
|
||||
created () {
|
||||
this.fetchGuilds();
|
||||
},
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '../../../../store';
|
||||
import groupUtilities from '../../../../mixins/groupsUtilities';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
|
||||
export default {
|
||||
mixins: [groupUtilities],
|
||||
props: ['guild'],
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
...mapState({user: 'user.data'}),
|
||||
isMember () {
|
||||
return this.isMemberOfGroup(this.user, this.guild);
|
||||
},
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
</style>
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import groupUtilities from '../../../mixins/groupsUtilities';
|
||||
import { mapState } from '../../../store';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
mixins: [groupUtilities],
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
...mapState({user: 'user.data'}),
|
||||
isMember () {
|
||||
return this.isMemberOfGroup(this.user, this.guild);
|
||||
},
|
||||
|
||||
@@ -8,7 +8,3 @@
|
||||
.col-12
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
@@ -17,19 +17,17 @@ li
|
||||
li(v-if="task.type === 'todo'") due date: {{task.date}}
|
||||
li attribute {{task.attribute}}
|
||||
li difficulty {{task.priority}}
|
||||
li tags {{taskTags}}
|
||||
li tags {{getTagsFor(task)}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['task'],
|
||||
computed: {
|
||||
taskTags () {
|
||||
let taskTags = this.task.tags;
|
||||
return this.$store.state.user.tags
|
||||
.filter(tag => taskTags.indexOf(tag.id) !== -1)
|
||||
.map(tag => tag.name);
|
||||
},
|
||||
...mapState({user: 'user.data'}),
|
||||
...mapGetters({getTagsFor: 'tasks:getTagsFor'}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<script>
|
||||
import Task from './task';
|
||||
import { mapState } from '../store';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['tasks']),
|
||||
...mapState({tasks: 'tasks.data'}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
45
website/client/libs/asyncResource.js
Normal file
45
website/client/libs/asyncResource.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import get from 'lodash/get';
|
||||
import axios from 'axios';
|
||||
|
||||
// Return an object used to describe, in the app state, an
|
||||
// async resource loaded from the server with its data and status.
|
||||
export function asyncResourceFactory () {
|
||||
return {
|
||||
loadingStatus: 'NOT_LOADED', // NOT_LOADED, LOADING, LOADED
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadAsyncResource ({store, path, url, deserialize, forceLoad = false}) {
|
||||
if (!store) throw new Error('"store" is required and must be the application store.');
|
||||
if (!path) throw new Error('The path to the resource in the application state is required.');
|
||||
if (!url) throw new Error('The resource\'s url on the server is required.');
|
||||
if (!deserialize) throw new Error('A response deserialization function named must be passed as "deserialize".');
|
||||
|
||||
const resource = get(store.state, path);
|
||||
if (!resource) throw new Error(`No resouce found at path "${path}".`);
|
||||
const loadingStatus = resource.loadingStatus;
|
||||
|
||||
if (loadingStatus === 'LOADED' && !forceLoad) {
|
||||
return Promise.resolve(resource);
|
||||
} else if (loadingStatus === 'LOADING') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const resourceWatcher = store.watch(state => get(state, `${path}.loadingStatus`), (newLoadingStatus) => {
|
||||
resourceWatcher(); // remove the watcher
|
||||
if (newLoadingStatus === 'LOADED') {
|
||||
return resolve(resource);
|
||||
} else {
|
||||
return reject(); // TODO add reason?
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (loadingStatus === 'NOT_LOADED' || loadingStatus === 'LOADED' && forceLoad) {
|
||||
return axios.get(url).then(response => { // TODO support more params
|
||||
resource.loadingStatus = 'LOADED';
|
||||
resource.data = deserialize(response);
|
||||
return resource;
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error(`Invalid loading status "${loadingStatus} for resource at "${path}".`));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
// Plugin to expose globally a '$t' method that calls common/i18n.t.
|
||||
// Can be used in templates.
|
||||
// Vue plugin to globally expose a '$t' method that calls common/i18n.t.
|
||||
// Can be anywhere inside vue as 'this.$t' or '$t' in templates.
|
||||
|
||||
import i18n from 'common/script/i18n';
|
||||
|
||||
import i18n from '../../common/script/i18n';
|
||||
// Load all english translations
|
||||
// TODO it's a workaround until proper translation loading works
|
||||
const context = require.context('../../common/locales/en', true, /\.(json)$/);
|
||||
const context = require.context('common/locales/en', true, /\.(json)$/);
|
||||
const translations = {};
|
||||
|
||||
context.keys().forEach(filename => {
|
||||
@@ -19,9 +19,11 @@ store implementation. mapMutations is not present because we do not use mutation
|
||||
|
||||
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.
|
||||
The code has been slightly changed to match our code style and to support nested paths.
|
||||
*/
|
||||
|
||||
import get from 'lodash/get';
|
||||
|
||||
function normalizeMap (map) {
|
||||
return Array.isArray(map) ?
|
||||
map.map(key => ({ key, val: key })) :
|
||||
@@ -35,7 +37,7 @@ export function mapState (states) {
|
||||
res[key] = function mappedState () {
|
||||
return typeof val === 'function' ?
|
||||
val.call(this, this.$store.state, this.$store.getters) :
|
||||
this.$store.state[val];
|
||||
get(this.$store.state, val);
|
||||
};
|
||||
});
|
||||
|
||||
83
website/client/libs/store/index.js
Normal file
83
website/client/libs/store/index.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
// 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.
|
||||
|
||||
export default class Store {
|
||||
constructor ({state, getters, actions}) {
|
||||
// Store actions
|
||||
this._actions = actions;
|
||||
|
||||
// Store getters (computed properties), implemented as computed properties in the internal Vue VM
|
||||
this.getters = {};
|
||||
|
||||
// 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(this);
|
||||
|
||||
Object.defineProperty(this.getters, key, {
|
||||
get: () => this._vm[key],
|
||||
});
|
||||
});
|
||||
|
||||
// Setup internal Vue instance to make state and getters reactive
|
||||
this._vm = new Vue({
|
||||
data: { state },
|
||||
computed: _computed,
|
||||
});
|
||||
}
|
||||
|
||||
// Return the store's state
|
||||
get state () {
|
||||
return this._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 = this._actions[type];
|
||||
|
||||
if (!action) throw new Error(`Action "${type}" not found.`);
|
||||
return action(this, ...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 this._vm.$watch(() => getter(this.state), cb, options);
|
||||
}
|
||||
|
||||
// Expose the store as this.$store in components
|
||||
// Is automatically called when Vue.plugin(Store) is used
|
||||
static install (_Vue) {
|
||||
_Vue.mixin({
|
||||
beforeCreate () {
|
||||
const options = this.$options;
|
||||
// store injection
|
||||
if (options.store) {
|
||||
this.$store = options.store;
|
||||
} else if (options.parent && options.parent.$store) {
|
||||
this.$store = options.parent.$store;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
mapState,
|
||||
mapGetters,
|
||||
mapActions,
|
||||
} from './helpers/public';
|
||||
@@ -6,9 +6,10 @@ import Vue from 'vue';
|
||||
import axios from 'axios';
|
||||
import AppComponent from './app';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import generateStore from './store';
|
||||
import StoreModule from './libs/store';
|
||||
import './filters/registerGlobals';
|
||||
import i18n from './plugins/i18n';
|
||||
import i18n from './libs/i18n';
|
||||
|
||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
|
||||
|
||||
@@ -23,8 +24,9 @@ Vue.config.performance = !IS_PRODUCTION;
|
||||
Vue.config.productionTip = IS_PRODUCTION;
|
||||
|
||||
Vue.use(i18n);
|
||||
Vue.use(StoreModule);
|
||||
|
||||
// TODO just for the beginning
|
||||
// TODO just until we have proper authentication
|
||||
let authSettings = localStorage.getItem('habit-mobile-settings');
|
||||
|
||||
if (authSettings) {
|
||||
@@ -33,32 +35,35 @@ if (authSettings) {
|
||||
axios.defaults.headers.common['x-api-key'] = authSettings.auth.apiToken;
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
export default new Vue({
|
||||
router,
|
||||
store: generateStore(),
|
||||
render: h => h(AppComponent),
|
||||
beforeCreate () {
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
// Load the user and the user tasks
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch'),
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).catch((err) => {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
alert('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.');
|
||||
});
|
||||
},
|
||||
mounted () { // Remove the loading screen when the app is mounted
|
||||
let loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||
},
|
||||
});
|
||||
|
||||
// Setup listener for title
|
||||
store.watch(state => state.title, (title) => {
|
||||
document.title = title;
|
||||
});
|
||||
|
||||
// Mount the app when user and tasks are loaded
|
||||
let userDataWatcher = store.watch(state => [state.user, state.tasks], ([user, tasks]) => {
|
||||
if (user && user._id && Array.isArray(tasks)) {
|
||||
userDataWatcher(); // remove the watcher
|
||||
app.$mount('#app');
|
||||
}
|
||||
});
|
||||
|
||||
// Load the user and the user tasks
|
||||
Promise.all([
|
||||
store.dispatch('user:fetch'),
|
||||
store.dispatch('tasks:fetchUserTasks'),
|
||||
]).catch(() => {
|
||||
alert('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.');
|
||||
});
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function fetchAll (store) {
|
||||
let response = await axios.get('/api/v3/groups?type=publicGuilds');
|
||||
store.state.guilds = response.data.data;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { flattenAndNamespace } from '../helpers/internals';
|
||||
import { flattenAndNamespace } from 'client/libs/store/helpers/internals';
|
||||
|
||||
import * as user from './user';
|
||||
import * as tasks from './tasks';
|
||||
import * as guilds from './guilds';
|
||||
|
||||
// Actions should be named as 'actionName' and can be accessed as 'namespace:actionName'
|
||||
// Example: fetch in user.js -> 'user:fetch'
|
||||
@@ -10,7 +9,6 @@ import * as guilds from './guilds';
|
||||
const actions = flattenAndNamespace({
|
||||
user,
|
||||
tasks,
|
||||
guilds,
|
||||
});
|
||||
|
||||
export default actions;
|
||||
@@ -1,6 +1,13 @@
|
||||
import axios from 'axios';
|
||||
import { loadAsyncResource } from 'client/libs/asyncResource';
|
||||
|
||||
export async function fetchUserTasks (store) {
|
||||
let response = await axios.get('/api/v3/tasks/user');
|
||||
store.state.tasks = response.data.data;
|
||||
export function fetchUserTasks (store, forceLoad = false) {
|
||||
return loadAsyncResource({
|
||||
store,
|
||||
path: 'tasks',
|
||||
url: '/api/v3/tasks/user',
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
forceLoad,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import axios from 'axios';
|
||||
import { loadAsyncResource } from 'client/libs/asyncResource';
|
||||
|
||||
export async function fetch (store) { // eslint-disable-line no-shadow
|
||||
let response = await axios.get('/api/v3/user');
|
||||
store.state.user = response.data.data;
|
||||
export function fetch (store, forceLoad = false) { // eslint-disable-line no-shadow
|
||||
return loadAsyncResource({
|
||||
store,
|
||||
path: 'user',
|
||||
url: '/api/v3/user',
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
forceLoad,
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { flattenAndNamespace } from '../helpers/internals';
|
||||
import { flattenAndNamespace } from 'client/libs/store/helpers/internals';
|
||||
import * as user from './user';
|
||||
import * as tasks from './tasks';
|
||||
|
||||
// Getters should be named as 'getterName' and can be accessed as 'namespace:getterName'
|
||||
// Example: gems in user.js -> 'user:gems'
|
||||
|
||||
const getters = flattenAndNamespace({
|
||||
user,
|
||||
tasks,
|
||||
});
|
||||
|
||||
export default getters;
|
||||
6
website/client/store/getters/tasks.js
Normal file
6
website/client/store/getters/tasks.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Return all the tags belonging to an user task
|
||||
export function getTagsFor (store) {
|
||||
return (task) => store.state.user.data.tags
|
||||
.filter(tag => task.tags.indexOf(tag.id) !== -1)
|
||||
.map(tag => tag.name);
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export function gems (store) {
|
||||
return store.state.user.balance * 4;
|
||||
return store.state.user.data.balance * 4;
|
||||
}
|
||||
@@ -1,76 +1,25 @@
|
||||
import Vue from 'vue';
|
||||
import state from './state';
|
||||
import Store from 'client/libs/store';
|
||||
import deepFreeze from 'client/libs/deepFreeze';
|
||||
import content from 'common/script/content/index';
|
||||
import { asyncResourceFactory } from 'client/libs/asyncResource';
|
||||
|
||||
import actions from './actions';
|
||||
import 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;
|
||||
// Export a function that generates the store and not the store directly
|
||||
// so that we can regenerate it multiple times for testing
|
||||
export default function () {
|
||||
return new Store({
|
||||
actions,
|
||||
getters,
|
||||
state: {
|
||||
title: 'Habitica',
|
||||
user: asyncResourceFactory(),
|
||||
tasks: asyncResourceFactory(), // user tasks
|
||||
// content data, frozen to prevent Vue from modifying it since it's static and never changes
|
||||
// TODO apply freezing to the entire codebase (the server) and not only to the client side?
|
||||
// NOTE this takes about 10-15ms on a fast computer
|
||||
content: deepFreeze(content),
|
||||
},
|
||||
// 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],
|
||||
});
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
||||
export {
|
||||
mapState,
|
||||
mapGetters,
|
||||
mapActions,
|
||||
} from './helpers/public';
|
||||
|
||||
// Setup internal Vue instance to make state and getters reactive
|
||||
_vm = new Vue({
|
||||
data: { state },
|
||||
computed: _computed,
|
||||
});
|
||||
|
||||
// Inject the store into all components as this.$store
|
||||
Vue.mixin({
|
||||
beforeCreate () {
|
||||
this.$store = store;
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import deepFreeze from '../libs/deepFreeze';
|
||||
import content from '../../common/script/content/index';
|
||||
|
||||
const state = {
|
||||
title: 'Habitica',
|
||||
user: null,
|
||||
tasks: null, // user tasks
|
||||
guilds: null, // list of public guilds, not fetched initially
|
||||
// content data, frozen to prevent Vue from modifying it since it's static and never changes
|
||||
// TODO apply freezing to the entire codebase (the server) and not only to the client side?
|
||||
// NOTE this takes about 10-15ms on a fast computer
|
||||
content: deepFreeze(content),
|
||||
};
|
||||
|
||||
export default state;
|
||||
Reference in New Issue
Block a user