mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
Merge branch 'develop' into release
This commit is contained in:
8
.babelrc
8
.babelrc
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
["transform-async-to-module-method", {
|
||||
"module": "bluebird",
|
||||
"method": "coroutine"
|
||||
}]
|
||||
"transform-es2015-modules-commonjs",
|
||||
"syntax-object-rest-spread",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ node_modules/**
|
||||
.bower-registry/**
|
||||
website/client-old/**
|
||||
website/client/**
|
||||
website/client/store/**
|
||||
website/views/**
|
||||
website/build/**
|
||||
dist/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
- '8'
|
||||
services:
|
||||
- mongodb
|
||||
cache:
|
||||
@@ -8,8 +8,6 @@ cache:
|
||||
- 'node_modules'
|
||||
addons:
|
||||
chrome: stable
|
||||
before_install:
|
||||
- npm install -g npm@5
|
||||
before_script:
|
||||
- npm run test:build
|
||||
- cp config.json.example config.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:boron
|
||||
FROM node:8
|
||||
|
||||
# Upgrade NPM to v5 (Yarn is needed because of this bug https://github.com/npm/npm/issues/16807)
|
||||
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:boron
|
||||
FROM node:8
|
||||
|
||||
ENV ADMIN_EMAIL admin@habitica.com
|
||||
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
|
||||
|
||||
@@ -2,7 +2,6 @@ import { exec } from 'child_process';
|
||||
import psTree from 'ps-tree';
|
||||
import nconf from 'nconf';
|
||||
import net from 'net';
|
||||
import Bluebird from 'bluebird';
|
||||
import { post } from 'superagent';
|
||||
import { sync as glob } from 'glob';
|
||||
import Mocha from 'mocha';
|
||||
@@ -45,7 +44,7 @@ export function kill (proc) {
|
||||
* before failing.
|
||||
*/
|
||||
export function awaitPort (port, max = 60) {
|
||||
return new Bluebird((rej, res) => {
|
||||
return new Promise((rej, res) => {
|
||||
let socket;
|
||||
let timeout;
|
||||
let interval;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
const authorName = 'Blade';
|
||||
const authorUuid = '75f270e8-c5db-4722-a5e6-a83f1b23f76b';
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
* pm'ed each user asking if they would like their tasks reset to the previous day
|
||||
***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* from an object to a number, hence this migration.
|
||||
***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
* and transfers a group's progress to it
|
||||
***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* <userid>@example.com
|
||||
***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
* they support a type and options and label
|
||||
* ***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* message into the chat for affected parties.
|
||||
***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const uuid = require('uuid');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
88
migrations/archive/2017/20171211_sanitize_emails.js
Normal file
88
migrations/archive/2017/20171211_sanitize_emails.js
Normal file
@@ -0,0 +1,88 @@
|
||||
var migrationName = '20171211_sanitize_emails.js';
|
||||
var authorName = 'Julius'; // in case script author needs to know when their ...
|
||||
var authorUuid = 'dd16c270-1d6d-44bd-b4f9-737342e79be6'; //... own data is done
|
||||
|
||||
/*
|
||||
User creation saves email as lowercase, but updating an email did not.
|
||||
Run this script to ensure all lowercased emails in db AFTER fix for updating emails is implemented.
|
||||
This will fix inconsistent querying for an email when attempting to password reset.
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
var query = {
|
||||
'auth.local.email': /[A-Z]/
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data)
|
||||
'auth.local.email'
|
||||
],
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var push;
|
||||
var set = {
|
||||
'auth.local.email': user.auth.local.email.toLowerCase()
|
||||
};
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -2,3 +2,5 @@ If you need to use a migration from this folder, move it to /migrations.
|
||||
|
||||
Note that /migrations files (excluding /archive) are linted, so to pass test you'll have to make sure
|
||||
that the file is written correctly.
|
||||
|
||||
They might also be using some old deps that we don't use anymore like Bluebird, mongoskin, ...
|
||||
@@ -1,5 +1,3 @@
|
||||
import Bluebird from 'Bluebird';
|
||||
|
||||
import { model as Challenges } from '../../website/server/models/challenge';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
@@ -17,10 +15,10 @@ async function syncChallengeToMembers (challenges) {
|
||||
promises.push(user.save());
|
||||
});
|
||||
|
||||
return Bluebird.all(promises);
|
||||
return Promise.all(promises);
|
||||
});
|
||||
|
||||
return await Bluebird.all(challengSyncPromises);
|
||||
return await Promise.all(challengSyncPromises);
|
||||
}
|
||||
|
||||
async function syncChallenges (lastChallengeDate) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
import { model as Group } from '../../website/server/models/group';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
@@ -16,7 +14,7 @@ async function createGroup (name, privacy, type, leaderId) {
|
||||
group.leader = user._id;
|
||||
user.guilds.push(group._id);
|
||||
|
||||
return Bluebird.all([group.save(), user.save()]);
|
||||
return Promise.all([group.save(), user.save()]);
|
||||
}
|
||||
|
||||
module.exports = async function groupCreator () {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
/*
|
||||
* This migration will find users with unlimited subscriptions who are also eligible for Jackalope mounts, and award them
|
||||
*/
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
import { model as Group } from '../../website/server/models/group';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
@@ -38,7 +37,7 @@ async function handOutJackalopes () {
|
||||
|
||||
cursor.on('close', async () => {
|
||||
console.log('done');
|
||||
return await Bluebird.all(promises);
|
||||
return await Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ let authorUuid = ''; // ... own data is done
|
||||
* subscription to all members
|
||||
*/
|
||||
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
import { model as Group } from '../../website/server/models/group';
|
||||
import * as payments from '../../website/server/libs/payments';
|
||||
|
||||
@@ -28,7 +26,7 @@ async function updateGroupsWithGroupPlans () {
|
||||
});
|
||||
|
||||
cursor.on('close', async () => {
|
||||
return await Bluebird.all(promises);
|
||||
return await Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
require('babel-register');
|
||||
require('babel-polyfill');
|
||||
|
||||
// This file must use ES5, everything required can be in ES6
|
||||
|
||||
function setUpServer () {
|
||||
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
|
||||
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
|
||||
const Bluebird = require('bluebird'); // eslint-disable-line global-require, no-unused-vars
|
||||
const setupNconf = require('../website/server/libs/setupNconf'); // eslint-disable-line global-require
|
||||
|
||||
setupNconf();
|
||||
|
||||
// We require src/server and npt src/index because
|
||||
// 1. nconf is already setup
|
||||
// 2. we don't need clustering
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
let Bluebird = require('bluebird');
|
||||
let request = require('superagent');
|
||||
let last = require('lodash/last');
|
||||
let AWS = require('aws-sdk');
|
||||
@@ -74,7 +73,7 @@ function uploadToS3 (start, end, filesUrls) {
|
||||
});
|
||||
console.log(promises.length);
|
||||
|
||||
return Bluebird.all(promises)
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
currentIndex += 50;
|
||||
uploadToS3(currentIndex, currentIndex + 50, filesUrls);
|
||||
|
||||
2148
package-lock.json
generated
2148
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -10,7 +10,7 @@
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"autoprefixer": "^8.1.0",
|
||||
"aws-sdk": "^2.209.0",
|
||||
"aws-sdk": "^2.211.0",
|
||||
"axios": "^0.18.0",
|
||||
"axios-progress-bar": "^1.1.8",
|
||||
"babel-core": "^6.0.0",
|
||||
@@ -18,7 +18,7 @@
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-regenerator": "^6.16.1",
|
||||
"babel-polyfill": "^6.6.1",
|
||||
@@ -26,7 +26,6 @@
|
||||
"babel-register": "^6.6.0",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"bcrypt": "^1.0.2",
|
||||
"bluebird": "^3.3.5",
|
||||
"body-parser": "^1.15.0",
|
||||
"bootstrap": "^4.0.0",
|
||||
"bootstrap-vue": "^2.0.0-rc.2",
|
||||
@@ -34,7 +33,7 @@
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"cross-env": "^5.1.4",
|
||||
"css-loader": "^0.28.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"csv-stringify": "^2.0.4",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
@@ -51,7 +50,7 @@
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
"hellojs": "^1.15.1",
|
||||
"html-webpack-plugin": "^2.8.1",
|
||||
"html-webpack-plugin": "^3.0.0",
|
||||
"image-size": "^0.6.2",
|
||||
"in-app-purchase": "^1.8.9",
|
||||
"intro.js": "^2.6.0",
|
||||
@@ -93,7 +92,7 @@
|
||||
"svgo": "^1.0.5",
|
||||
"svgo-loader": "^2.1.0",
|
||||
"universal-analytics": "^0.4.16",
|
||||
"url-loader": "^0.6.2",
|
||||
"url-loader": "^1.0.0",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.0.1",
|
||||
"validator": "^9.4.1",
|
||||
@@ -114,8 +113,8 @@
|
||||
},
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^6.9.1",
|
||||
"npm": "^5.0.0"
|
||||
"node": "^8.9.4",
|
||||
"npm": "^5.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
@@ -142,7 +141,9 @@
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^1.0.0-beta.12",
|
||||
"babel-plugin-istanbul": "^4.1.6",
|
||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chalk": "^2.3.2",
|
||||
@@ -150,15 +151,15 @@
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^3.0.0",
|
||||
"cross-spawn": "^6.0.5",
|
||||
"eslint": "^4.18.2",
|
||||
"eslint": "^4.19.0",
|
||||
"eslint-config-habitrpg": "^4.0.0",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-loader": "^1.3.0",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-html": "^4.0.2",
|
||||
"eslint-plugin-mocha": "^4.12.1",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"expect.js": "^0.3.1",
|
||||
"http-proxy-middleware": "^0.17.0",
|
||||
"http-proxy-middleware": "^0.18.0",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"karma": "^2.0.0",
|
||||
"karma-babel-preprocessor": "^7.0.0",
|
||||
@@ -176,11 +177,11 @@
|
||||
"mocha": "^5.0.4",
|
||||
"monk": "^6.0.5",
|
||||
"nightwatch": "^0.9.20",
|
||||
"puppeteer": "^1.1.1",
|
||||
"puppeteer": "^1.2.0",
|
||||
"require-again": "^2.0.0",
|
||||
"selenium-server": "^3.11.0",
|
||||
"sinon": "^4.4.5",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-chai": "^3.0.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"webpack-bundle-analyzer": "^2.11.1",
|
||||
"webpack-dev-middleware": "^2.0.5",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require('babel-register');
|
||||
require('babel-polyfill');
|
||||
|
||||
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
|
||||
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
|
||||
|
||||
@@ -151,7 +151,10 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
});
|
||||
|
||||
officialChallenge = await generateChallenge(user, group, {
|
||||
official: true,
|
||||
categories: [{
|
||||
name: 'habitica_official',
|
||||
slug: 'habitica_official',
|
||||
}],
|
||||
});
|
||||
|
||||
challenge = await generateChallenge(user, group);
|
||||
|
||||
@@ -193,7 +193,10 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
officialChallenge = await generateChallenge(user, group, {
|
||||
official: true,
|
||||
categories: [{
|
||||
name: 'habitica_official',
|
||||
slug: 'habitica_official',
|
||||
}],
|
||||
});
|
||||
|
||||
challenge = await generateChallenge(user, group);
|
||||
|
||||
@@ -2,13 +2,12 @@ import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import xml2js from 'xml2js';
|
||||
import Bluebird from 'bluebird';
|
||||
import util from 'util';
|
||||
|
||||
let parseStringAsync = Bluebird.promisify(xml2js.parseString, {context: xml2js});
|
||||
let parseStringAsync = util.promisify(xml2js.parseString).bind(xml2js);
|
||||
|
||||
describe('GET /export/userdata.xml', () => {
|
||||
// TODO disabled because it randomly causes the build to fail
|
||||
xit('should return a valid XML file with user data', async () => {
|
||||
it('should return a valid XML file with user data', async () => {
|
||||
let user = await generateUser();
|
||||
let tasks = await user.post('/tasks/user', [
|
||||
{type: 'habit', text: 'habit 1'},
|
||||
@@ -31,13 +30,21 @@ describe('GET /export/userdata.xml', () => {
|
||||
expect(res).to.contain.all.keys(['tasks', 'flags', 'tasksOrder', 'auth']);
|
||||
expect(res.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(res.tasks).to.have.all.keys(['dailys', 'habits', 'todos', 'rewards']);
|
||||
|
||||
expect(res.tasks.habits.length).to.equal(2);
|
||||
expect(res.tasks.habits[0]._id).to.equal(tasks[0]._id);
|
||||
let habitIds = _.map(res.tasks.habits, '_id');
|
||||
expect(habitIds).to.have.deep.members([tasks[0]._id, tasks[4]._id]);
|
||||
|
||||
expect(res.tasks.dailys.length).to.equal(2);
|
||||
expect(res.tasks.dailys[0]._id).to.equal(tasks[1]._id);
|
||||
let dailyIds = _.map(res.tasks.dailys, '_id');
|
||||
expect(dailyIds).to.have.deep.members([tasks[1]._id, tasks[5]._id]);
|
||||
|
||||
expect(res.tasks.rewards.length).to.equal(2);
|
||||
expect(res.tasks.rewards[0]._id).to.equal(tasks[2]._id);
|
||||
let rewardIds = _.map(res.tasks.rewards, '_id');
|
||||
expect(rewardIds).to.have.deep.members([tasks[2]._id, tasks[6]._id]);
|
||||
|
||||
expect(res.tasks.todos.length).to.equal(3);
|
||||
expect(res.tasks.todos[1]._id).to.equal(tasks[3]._id);
|
||||
let todoIds = _.map(res.tasks.todos, '_id');
|
||||
expect(todoIds).to.deep.include.members([tasks[3]._id, tasks[7]._id]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,8 +201,8 @@ describe('GET /groups', () => {
|
||||
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
|
||||
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
|
||||
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
|
||||
.to.eventually.have.a.lengthOf(1 + 3); // 1 created now, 3 by other tests
|
||||
expect(page2[3].name).to.equal('guild with less members');
|
||||
.to.eventually.have.a.lengthOf(1 + 4); // 1 created now, 4 by other tests
|
||||
expect(page2[4].name).to.equal('guild with less members');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,4 +220,18 @@ describe('GET /groups', () => {
|
||||
await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
|
||||
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW);
|
||||
});
|
||||
|
||||
it('returns a list of groups user has access to', async () => {
|
||||
let group = await generateGroup(user, {
|
||||
name: 'c++ coders',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
});
|
||||
|
||||
// search for 'c++ coders'
|
||||
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders'))
|
||||
.to.eventually.have.lengthOf(1)
|
||||
.and.to.have.nested.property('[0]')
|
||||
.and.to.have.property('_id', group._id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,12 +44,12 @@ describe('POST /group/:groupId/join', () => {
|
||||
expect(res.leader.profile.name).to.eql(user.profile.name);
|
||||
});
|
||||
|
||||
it('returns an error is user was already a member', async () => {
|
||||
it('returns an error if user was already a member', async () => {
|
||||
await joiningUser.post(`/groups/${publicGuild._id}/join`);
|
||||
await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('userAlreadyInGroup'),
|
||||
message: t('youAreAlreadyInGroup'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -262,6 +262,30 @@ describe('POST /group/:groupId/join', () => {
|
||||
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(false);
|
||||
});
|
||||
|
||||
it('does not allow user to leave a party if a quest was active and they were the only member', async () => {
|
||||
let userToInvite = await generateUser();
|
||||
let oldParty = await userToInvite.post('/groups', { // add user to a party
|
||||
name: 'Another Test Party',
|
||||
type: 'party',
|
||||
});
|
||||
|
||||
await userToInvite.update({
|
||||
[`items.quests.${PET_QUEST}`]: 1,
|
||||
});
|
||||
await userToInvite.post(`/groups/${oldParty._id}/quests/invite/${PET_QUEST}`);
|
||||
|
||||
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true);
|
||||
await user.post(`/groups/${party._id}/invite`, {
|
||||
uuids: [userToInvite._id],
|
||||
});
|
||||
|
||||
await expect(userToInvite.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageCannotLeaveWhileQuesting'),
|
||||
});
|
||||
});
|
||||
|
||||
it('invites joining member to active quest', async () => {
|
||||
await user.update({
|
||||
[`items.quests.${PET_QUEST}`]: 1,
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
generateUser,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
describe('POST /groups/:groupId/quests/accept', () => {
|
||||
const PET_QUEST = 'whale';
|
||||
@@ -140,7 +140,7 @@ describe('POST /groups/:groupId/quests/accept', () => {
|
||||
// quest will start after everyone has accepted
|
||||
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await Bluebird.delay(500);
|
||||
await sleep(0.5);
|
||||
|
||||
await rejectingMember.sync();
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
generateUser,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
const PET_QUEST = 'whale';
|
||||
@@ -135,7 +135,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await Bluebird.delay(500);
|
||||
await sleep(0.5);
|
||||
|
||||
await Promise.all([
|
||||
partyMemberThatRejects.sync(),
|
||||
@@ -161,7 +161,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await Bluebird.delay(500);
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await Bluebird.delay(500);
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
@@ -201,7 +201,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await Bluebird.delay(500);
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
@@ -222,7 +222,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await Bluebird.delay(500);
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
generateUser,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
describe('POST /groups/:groupId/quests/reject', () => {
|
||||
let questingGroup;
|
||||
@@ -168,7 +168,7 @@ describe('POST /groups/:groupId/quests/reject', () => {
|
||||
// quest will start after everyone has accepted or rejected
|
||||
await rejectingMember.post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
|
||||
await Bluebird.delay(500);
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
describe('GET /shops/market', () => {
|
||||
let user;
|
||||
@@ -42,13 +41,13 @@ describe('GET /shops/market', () => {
|
||||
return array;
|
||||
}, []);
|
||||
|
||||
let results = await Bluebird.each(items, (item) => {
|
||||
let results = await Promise.all(items.map((item) => {
|
||||
let { purchaseType, key } = item;
|
||||
return user.post(`/user/purchase/${purchaseType}/${key}`);
|
||||
});
|
||||
}));
|
||||
|
||||
expect(results.length).to.be.greaterThan(0);
|
||||
results.forEach((item) => {
|
||||
items.forEach((item) => {
|
||||
expect(item).to.include.keys('key', 'text', 'notes', 'class', 'value', 'currency');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
sleep,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import Bluebird from 'bluebird';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/score/:direction', () => {
|
||||
@@ -27,7 +27,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
await sleep(1);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.habits[0];
|
||||
});
|
||||
@@ -65,7 +65,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
await sleep(1);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.dailys[0];
|
||||
});
|
||||
@@ -109,7 +109,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
await sleep(1);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.todos[0];
|
||||
});
|
||||
@@ -134,7 +134,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
await sleep(1);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.todos[0];
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
each,
|
||||
map,
|
||||
} from 'lodash';
|
||||
import Bluebird from 'bluebird';
|
||||
import {
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
@@ -104,7 +103,7 @@ describe('DELETE /user', () => {
|
||||
password,
|
||||
});
|
||||
|
||||
await Bluebird.all(map(ids, id => {
|
||||
await Promise.all(map(ids, id => {
|
||||
return expect(checkExistence('tasks', id)).to.eventually.eql(false);
|
||||
}));
|
||||
});
|
||||
|
||||
24
test/api/v3/integration/user/GET-user_inAppRewards.test.js
Normal file
24
test/api/v3/integration/user/GET-user_inAppRewards.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('GET /user/in-app-rewards', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns the reward items available for purchase', async () => {
|
||||
let buyList = await user.get('/user/in-app-rewards');
|
||||
|
||||
expect(_.find(buyList, item => {
|
||||
return item.text === t('armorWarrior1Text');
|
||||
})).to.exist;
|
||||
|
||||
expect(_.find(buyList, item => {
|
||||
return item.text === t('armorWarrior2Text');
|
||||
})).to.not.exist;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('GET /user/toggle-pinned-item', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('cannot unpin potion', async () => {
|
||||
await expect(user.get('/user/toggle-pinned-item/potion/potion'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('cannotUnpinArmoirPotion'),
|
||||
});
|
||||
});
|
||||
|
||||
it('can pin shield_rogue_5', async () => {
|
||||
let result = await user.get('/user/toggle-pinned-item/marketGear/gear.flat.shield_rogue_5');
|
||||
|
||||
expect(result.pinnedItems.length).to.be.eql(user.pinnedItems.length + 1);
|
||||
});
|
||||
});
|
||||
@@ -258,11 +258,31 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
expect(user.achievements.birthday).to.equal(1);
|
||||
});
|
||||
|
||||
it('passes correct target to spell when targetType === \'task\'', async () => {
|
||||
await user.update({'stats.class': 'wizard', 'stats.lvl': 11});
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
let result = await user.post(`/user/class/cast/fireball?targetId=${task._id}`);
|
||||
|
||||
expect(result.task._id).to.equal(task._id);
|
||||
});
|
||||
|
||||
it('passes correct target to spell when targetType === \'self\'', async () => {
|
||||
await user.update({'stats.class': 'wizard', 'stats.lvl': 14, 'stats.mp': 50});
|
||||
|
||||
let result = await user.post('/user/class/cast/frost');
|
||||
|
||||
expect(result.user.stats.mp).to.equal(10);
|
||||
});
|
||||
|
||||
|
||||
// TODO find a way to have sinon working in integration tests
|
||||
// it doesn't work when tests are running separately from server
|
||||
it('passes correct target to spell when targetType === \'task\'');
|
||||
it('passes correct target to spell when targetType === \'tasks\'');
|
||||
it('passes correct target to spell when targetType === \'self\'');
|
||||
it('passes correct target to spell when targetType === \'party\'');
|
||||
it('passes correct target to spell when targetType === \'user\'');
|
||||
it('passes correct target to spell when targetType === \'party\' and user is not in a party');
|
||||
|
||||
@@ -30,10 +30,12 @@ describe('POST /user/release-both', () => {
|
||||
'items.currentPet': animal,
|
||||
'items.pets': loadPets(),
|
||||
'items.mounts': loadMounts(),
|
||||
'achievements.triadBingo': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when user balance is too low and user does not have triadBingo', async () => {
|
||||
// @TODO: Traid is now free. Add this back if we need
|
||||
xit('returns an error when user balance is too low and user does not have triadBingo', async () => {
|
||||
await expect(user.post('/user/release-both'))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
@@ -45,9 +47,7 @@ describe('POST /user/release-both', () => {
|
||||
// More tests in common code unit tests
|
||||
|
||||
it('grants triad bingo with gems', async () => {
|
||||
await user.update({
|
||||
balance: 1.5,
|
||||
});
|
||||
await user.update();
|
||||
|
||||
let response = await user.post('/user/release-both');
|
||||
await user.sync();
|
||||
|
||||
@@ -27,6 +27,33 @@ describe('PUT /user', () => {
|
||||
expect(user.stats.hp).to.eql(14);
|
||||
});
|
||||
|
||||
it('tags must be an array', async () => {
|
||||
await expect(user.put('/user', {
|
||||
tags: {
|
||||
tag: true,
|
||||
},
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'mustBeArray',
|
||||
});
|
||||
});
|
||||
|
||||
it('update tags', async () => {
|
||||
let userTags = user.tags;
|
||||
|
||||
await user.put('/user', {
|
||||
tags: [...user.tags, {
|
||||
name: 'new tag',
|
||||
}],
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.tags.length).to.be.eql(userTags.length + 1);
|
||||
});
|
||||
|
||||
|
||||
it('profile.name cannot be an empty string or null', async () => {
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': ' ', // string should be trimmed
|
||||
|
||||
@@ -357,6 +357,21 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sanitizes email params to a lowercase string before creating the user', async () => {
|
||||
let username = generateRandomUserName();
|
||||
let email = 'ISANEmAiL@ExAmPle.coM';
|
||||
let password = 'password';
|
||||
|
||||
let user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
expect(user.auth.local.email).to.equal(email.toLowerCase());
|
||||
});
|
||||
|
||||
it('fails on a habitica.com email', async () => {
|
||||
let username = generateRandomUserName();
|
||||
let email = `${username}@habitica.com`;
|
||||
|
||||
@@ -13,7 +13,7 @@ import nconf from 'nconf';
|
||||
const ENDPOINT = '/user/auth/update-email';
|
||||
|
||||
describe('PUT /user/auth/update-email', () => {
|
||||
let newEmail = 'some-new-email_2@example.net';
|
||||
let newEmail = 'SOmE-nEw-emAIl_2@example.net';
|
||||
let oldPassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
|
||||
|
||||
context('Local Authenticaion User', async () => {
|
||||
@@ -53,14 +53,15 @@ describe('PUT /user/auth/update-email', () => {
|
||||
});
|
||||
|
||||
it('changes email if new email and existing password are provided', async () => {
|
||||
let lowerCaseNewEmail = newEmail.toLowerCase();
|
||||
let response = await user.put(ENDPOINT, {
|
||||
newEmail,
|
||||
password: oldPassword,
|
||||
});
|
||||
expect(response).to.eql({ email: 'some-new-email_2@example.net' });
|
||||
expect(response.email).to.eql(lowerCaseNewEmail);
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.email).to.eql(newEmail);
|
||||
expect(user.auth.local.email).to.eql(lowerCaseNewEmail);
|
||||
});
|
||||
|
||||
it('rejects if email is already taken', async () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable global-require */
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import Bluebird from 'bluebird';
|
||||
import requireAgain from 'require-again';
|
||||
import { recoverCron, cron } from '../../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
@@ -1363,7 +1362,7 @@ describe('recoverCron', () => {
|
||||
});
|
||||
|
||||
it('throws an error if user cannot be found', async () => {
|
||||
execStub.returns(Bluebird.resolve(null));
|
||||
execStub.returns(Promise.resolve(null));
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
@@ -1374,8 +1373,8 @@ describe('recoverCron', () => {
|
||||
});
|
||||
|
||||
it('increases status.times count and reruns up to 4 times', async () => {
|
||||
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
execStub.onCall(4).returns(Bluebird.resolve({_cronSignature: 'NOT_RUNNING'}));
|
||||
execStub.returns(Promise.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
execStub.onCall(4).returns(Promise.resolve({_cronSignature: 'NOT_RUNNING'}));
|
||||
|
||||
await recoverCron(status, locals);
|
||||
|
||||
@@ -1384,7 +1383,7 @@ describe('recoverCron', () => {
|
||||
});
|
||||
|
||||
it('throws an error if recoverCron runs 5 times', async () => {
|
||||
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
execStub.returns(Promise.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('slack', () => {
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: 'flagger (flagger-id) flagged a message (language: flagger-lang)',
|
||||
text: 'flagger (flagger-id; language: flagger-lang) flagged a message',
|
||||
attachments: [{
|
||||
fallback: 'Flag Message',
|
||||
color: 'danger',
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
attachTranslateFunction,
|
||||
} from '../../../../../website/server/middlewares/language';
|
||||
import common from '../../../../../website/common';
|
||||
import Bluebird from 'bluebird';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
|
||||
const i18n = common.i18n;
|
||||
@@ -162,7 +161,7 @@ describe('language middleware', () => {
|
||||
return this;
|
||||
},
|
||||
exec () {
|
||||
return Bluebird.resolve({
|
||||
return Promise.resolve({
|
||||
preferences: {
|
||||
language: 'it',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Bluebird from 'bluebird';
|
||||
import moment from 'moment';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
@@ -123,7 +122,7 @@ describe('User Model', () => {
|
||||
it('adds notifications without data for all given users via static method', async () => {
|
||||
let user = new User();
|
||||
let otherUser = new User();
|
||||
await Bluebird.all([user.save(), otherUser.save()]);
|
||||
await Promise.all([user.save(), otherUser.save()]);
|
||||
|
||||
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON');
|
||||
|
||||
@@ -149,7 +148,7 @@ describe('User Model', () => {
|
||||
it('adds notifications with data and seen status for all given users via static method', async () => {
|
||||
let user = new User();
|
||||
let otherUser = new User();
|
||||
await Bluebird.all([user.save(), otherUser.save()]);
|
||||
await Promise.all([user.save(), otherUser.save()]);
|
||||
|
||||
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON', {field: 1}, true);
|
||||
|
||||
|
||||
211
test/client/unit/specs/components/tasks/column.js
Normal file
211
test/client/unit/specs/components/tasks/column.js
Normal file
@@ -0,0 +1,211 @@
|
||||
import { shallow, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import TaskColumn from 'client/components/tasks/column.vue';
|
||||
|
||||
import Store from 'client/libs/store';
|
||||
|
||||
// eslint-disable no-exclusive-tests
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Store);
|
||||
|
||||
describe('Task Column', () => {
|
||||
let wrapper;
|
||||
let store, getters;
|
||||
let habits, taskListOverride, tasks;
|
||||
|
||||
function makeWrapper (additionalSetup = {}) {
|
||||
let type = 'habit';
|
||||
let mocks = {
|
||||
$t () {},
|
||||
};
|
||||
let stubs = ['b-modal']; // <b-modal> is a custom component and not tested here
|
||||
|
||||
return shallow(TaskColumn, {
|
||||
propsData: {
|
||||
type,
|
||||
},
|
||||
mocks,
|
||||
stubs,
|
||||
localVue,
|
||||
...additionalSetup,
|
||||
});
|
||||
}
|
||||
|
||||
it('returns a vue instance', () => {
|
||||
wrapper = makeWrapper();
|
||||
expect(wrapper.isVueInstance()).to.be.true;
|
||||
});
|
||||
|
||||
describe('Passed Properties', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = makeWrapper();
|
||||
});
|
||||
|
||||
it('defaults isUser to false', () => {
|
||||
expect(wrapper.vm.isUser).to.be.false;
|
||||
});
|
||||
|
||||
it('passes isUser to component instance', () => {
|
||||
wrapper.setProps({ isUser: false });
|
||||
|
||||
expect(wrapper.vm.isUser).to.be.false;
|
||||
|
||||
wrapper.setProps({ isUser: true });
|
||||
|
||||
expect(wrapper.vm.isUser).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed Properties', () => {
|
||||
beforeEach(() => {
|
||||
habits = [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
];
|
||||
|
||||
taskListOverride = [
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
];
|
||||
|
||||
getters = {
|
||||
// (...) => { ... } will return a value
|
||||
// (...) => (...) => { ... } will return a function
|
||||
// Task Column expects a function
|
||||
'tasks:getFilteredTaskList': () => () => habits,
|
||||
};
|
||||
|
||||
store = new Store({getters});
|
||||
|
||||
wrapper = makeWrapper({store});
|
||||
});
|
||||
|
||||
it('returns task list from props for group-plan', () => {
|
||||
wrapper.setProps({ taskListOverride });
|
||||
|
||||
wrapper.vm.taskList.forEach((el, i) => {
|
||||
expect(el).to.eq(taskListOverride[i]);
|
||||
});
|
||||
|
||||
wrapper.setProps({ isUser: false, taskListOverride });
|
||||
|
||||
wrapper.vm.taskList.forEach((el, i) => {
|
||||
expect(el).to.eq(taskListOverride[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns task list from store for user', () => {
|
||||
wrapper.setProps({ isUser: true, taskListOverride });
|
||||
|
||||
wrapper.vm.taskList.forEach((el, i) => {
|
||||
expect(el).to.eq(habits[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods', () => {
|
||||
describe('Filter By Tags', () => {
|
||||
beforeEach(() => {
|
||||
tasks = [
|
||||
{ tags: [3, 4] },
|
||||
{ tags: [2, 3] },
|
||||
{ tags: [] },
|
||||
{ tags: [1, 3] },
|
||||
];
|
||||
});
|
||||
|
||||
it('returns all tasks if no tag is given', () => {
|
||||
let returnedTasks = wrapper.vm.filterByTagList(tasks);
|
||||
expect(returnedTasks).to.have.lengthOf(tasks.length);
|
||||
tasks.forEach((task, i) => {
|
||||
expect(returnedTasks[i]).to.eq(task);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns tasks for given single tag', () => {
|
||||
let returnedTasks = wrapper.vm.filterByTagList(tasks, [3]);
|
||||
|
||||
expect(returnedTasks).to.have.lengthOf(3);
|
||||
expect(returnedTasks[0]).to.eq(tasks[0]);
|
||||
expect(returnedTasks[1]).to.eq(tasks[1]);
|
||||
expect(returnedTasks[2]).to.eq(tasks[3]);
|
||||
});
|
||||
|
||||
it('returns tasks for given multiple tags', () => {
|
||||
let returnedTasks = wrapper.vm.filterByTagList(tasks, [2, 3]);
|
||||
|
||||
expect(returnedTasks).to.have.lengthOf(1);
|
||||
expect(returnedTasks[0]).to.eq(tasks[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter By Search Text', () => {
|
||||
beforeEach(() => {
|
||||
tasks = [
|
||||
{
|
||||
text: 'Hello world 1',
|
||||
note: '',
|
||||
checklist: [],
|
||||
},
|
||||
{
|
||||
text: 'Hello world 2',
|
||||
note: '',
|
||||
checklist: [],
|
||||
},
|
||||
{
|
||||
text: 'Generic Task Title',
|
||||
note: '',
|
||||
checklist: [
|
||||
{ text: 'Check 1' },
|
||||
{ text: 'Check 2' },
|
||||
{ text: 'Check 3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Hello world 3',
|
||||
note: 'Generic Task Note',
|
||||
checklist: [
|
||||
{ text: 'Checkitem 1' },
|
||||
{ text: 'Checkitem 2' },
|
||||
{ text: 'Checkitem 3' },
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it('returns all tasks for empty search term', () => {
|
||||
let returnedTasks = wrapper.vm.filterBySearchText(tasks);
|
||||
expect(returnedTasks).to.have.lengthOf(tasks.length);
|
||||
tasks.forEach((task, i) => {
|
||||
expect(returnedTasks[i]).to.eq(task);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns tasks for search term in title /i', () => {
|
||||
['Title', 'TITLE', 'title', 'tItLe'].forEach((term) => {
|
||||
expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[2]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns tasks for search term in note /i', () => {
|
||||
['Note', 'NOTE', 'note', 'nOtE'].forEach((term) => {
|
||||
expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns tasks for search term in checklist title /i', () => {
|
||||
['Check', 'CHECK', 'check', 'cHeCK'].forEach((term) => {
|
||||
let returnedTasks = wrapper.vm.filterBySearchText(tasks, term);
|
||||
|
||||
expect(returnedTasks[0]).to.eq(tasks[2]);
|
||||
expect(returnedTasks[1]).to.eq(tasks[3]);
|
||||
});
|
||||
|
||||
['Checkitem', 'CHECKITEM', 'checkitem', 'cHeCKiTEm'].forEach((term) => {
|
||||
expect(wrapper.vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
62
test/client/unit/specs/libs/store/helpers/filterTasks.js
Normal file
62
test/client/unit/specs/libs/store/helpers/filterTasks.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
getTypeLabel,
|
||||
getFilterLabels,
|
||||
getActiveFilter,
|
||||
} from 'client/libs/store/helpers/filterTasks.js';
|
||||
|
||||
describe('Filter Category for Tasks', () => {
|
||||
describe('getTypeLabel', () => {
|
||||
it('should return correct task type labels', () => {
|
||||
expect(getTypeLabel('habit')).to.eq('habits');
|
||||
expect(getTypeLabel('daily')).to.eq('dailies');
|
||||
expect(getTypeLabel('todo')).to.eq('todos');
|
||||
expect(getTypeLabel('reward')).to.eq('rewards');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFilterLabels', () => {
|
||||
let habit, daily, todo, reward;
|
||||
beforeEach(() => {
|
||||
habit = ['all', 'yellowred', 'greenblue'];
|
||||
daily = ['all', 'due', 'notDue'];
|
||||
todo = ['remaining', 'scheduled', 'complete2'];
|
||||
reward = ['all', 'custom', 'wishlist'];
|
||||
});
|
||||
|
||||
it('should return all task type filter labels by type', () => {
|
||||
// habits
|
||||
getFilterLabels('habit').forEach((item, i) => {
|
||||
expect(item).to.eq(habit[i]);
|
||||
});
|
||||
// dailys
|
||||
getFilterLabels('daily').forEach((item, i) => {
|
||||
expect(item).to.eq(daily[i]);
|
||||
});
|
||||
// todos
|
||||
getFilterLabels('todo').forEach((item, i) => {
|
||||
expect(item).to.eq(todo[i]);
|
||||
});
|
||||
// rewards
|
||||
getFilterLabels('reward').forEach((item, i) => {
|
||||
expect(item).to.eq(reward[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveFilter', () => {
|
||||
it('should return single function by default', () => {
|
||||
let activeFilter = getActiveFilter('habit');
|
||||
expect(activeFilter).to.be.an('object');
|
||||
expect(activeFilter).to.have.all.keys('label', 'filterFn', 'default');
|
||||
expect(activeFilter.default).to.be.true;
|
||||
});
|
||||
|
||||
it('should return single function for given filter type', () => {
|
||||
let activeFilterLabel = 'yellowred';
|
||||
let activeFilter = getActiveFilter('habit', activeFilterLabel);
|
||||
expect(activeFilter).to.be.an('object');
|
||||
expect(activeFilter).to.have.all.keys('label', 'filterFn');
|
||||
expect(activeFilter.label).to.eq(activeFilterLabel);
|
||||
});
|
||||
});
|
||||
});
|
||||
43
test/client/unit/specs/libs/store/helpers/orderTasks.js
Normal file
43
test/client/unit/specs/libs/store/helpers/orderTasks.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
orderSingleTypeTasks,
|
||||
// orderMultipleTypeTasks,
|
||||
} from 'client/libs/store/helpers/orderTasks.js';
|
||||
|
||||
import shuffle from 'lodash/shuffle';
|
||||
|
||||
describe('Task Order Helper Function', () => {
|
||||
let tasks, shuffledTasks, taskOrderList;
|
||||
beforeEach(() => {
|
||||
taskOrderList = [1, 2, 3, 4];
|
||||
tasks = [];
|
||||
taskOrderList.forEach(i => tasks.push({ _id: i, id: i }));
|
||||
shuffledTasks = shuffle(tasks);
|
||||
});
|
||||
|
||||
it('should return tasks as is for no task order', () => {
|
||||
expect(orderSingleTypeTasks(shuffledTasks)).to.eq(shuffledTasks);
|
||||
});
|
||||
|
||||
it('should return tasks in expected order', () => {
|
||||
let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList);
|
||||
newOrderedTasks.forEach((item, index) => {
|
||||
expect(item).to.eq(tasks[index]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return new tasks at end of expected order', () => {
|
||||
let newTaskIds = [10, 15, 20];
|
||||
newTaskIds.forEach(i => tasks.push({ _id: i, id: i }));
|
||||
shuffledTasks = shuffle(tasks);
|
||||
|
||||
let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList);
|
||||
// checking tasks with order
|
||||
newOrderedTasks.slice(0, taskOrderList.length).forEach((item, index) => {
|
||||
expect(item).to.eq(tasks[index]);
|
||||
});
|
||||
// check for new task ids
|
||||
newOrderedTasks.slice(-3).forEach(item => {
|
||||
expect(item.id).to.be.oneOf(newTaskIds);
|
||||
});
|
||||
});
|
||||
});
|
||||
118
test/client/unit/specs/store/getters/tasks/getTasks.js
Normal file
118
test/client/unit/specs/store/getters/tasks/getTasks.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import generateStore from 'client/store';
|
||||
|
||||
describe('Store Getters for Tasks', () => {
|
||||
let store, habits, dailys, todos, rewards;
|
||||
|
||||
beforeEach(() => {
|
||||
store = generateStore();
|
||||
// Get user preference data and user tasks order data
|
||||
store.state.user.data = {
|
||||
preferences: {},
|
||||
tasksOrder: {
|
||||
habits: [],
|
||||
dailys: [],
|
||||
todos: [],
|
||||
rewards: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('Task List', () => {
|
||||
beforeEach(() => {
|
||||
habits = [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
];
|
||||
dailys = [
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
];
|
||||
todos = [
|
||||
{ id: 5 },
|
||||
{ id: 6 },
|
||||
];
|
||||
rewards = [
|
||||
{ id: 7 },
|
||||
{ id: 8 },
|
||||
];
|
||||
store.state.tasks.data = {
|
||||
habits,
|
||||
dailys,
|
||||
todos,
|
||||
rewards,
|
||||
};
|
||||
});
|
||||
|
||||
it('should returns all tasks by task type', () => {
|
||||
let returnedTasks = store.getters['tasks:getUnfilteredTaskList']('habit');
|
||||
expect(returnedTasks).to.eq(habits);
|
||||
|
||||
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('daily');
|
||||
expect(returnedTasks).to.eq(dailys);
|
||||
|
||||
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('todo');
|
||||
expect(returnedTasks).to.eq(todos);
|
||||
|
||||
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('reward');
|
||||
expect(returnedTasks).to.eq(rewards);
|
||||
});
|
||||
});
|
||||
|
||||
// @TODO add task filter check for rewards and dailys
|
||||
describe('Task Filters', () => {
|
||||
beforeEach(() => {
|
||||
habits = [
|
||||
// weak habit
|
||||
{ value: 0 },
|
||||
// strong habit
|
||||
{ value: 2 },
|
||||
];
|
||||
todos = [
|
||||
// scheduled todos
|
||||
{ completed: false, date: 'Mon, 15 Jan 2018 12:18:29 GMT' },
|
||||
// completed todos
|
||||
{ completed: true },
|
||||
];
|
||||
store.state.tasks.data = {
|
||||
habits,
|
||||
todos,
|
||||
};
|
||||
});
|
||||
|
||||
it('should return weak habits', () => {
|
||||
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||
type: 'habit',
|
||||
filterType: 'yellowred',
|
||||
});
|
||||
|
||||
expect(returnedTasks[0]).to.eq(habits[0]);
|
||||
});
|
||||
|
||||
it('should return strong habits', () => {
|
||||
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||
type: 'habit',
|
||||
filterType: 'greenblue',
|
||||
});
|
||||
|
||||
expect(returnedTasks[0]).to.eq(habits[1]);
|
||||
});
|
||||
|
||||
it('should return scheduled todos', () => {
|
||||
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||
type: 'todo',
|
||||
filterType: 'scheduled',
|
||||
});
|
||||
|
||||
expect(returnedTasks[0]).to.eq(todos[0]);
|
||||
});
|
||||
|
||||
it('should return completed todos', () => {
|
||||
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||
type: 'todo',
|
||||
filterType: 'complete2',
|
||||
});
|
||||
|
||||
expect(returnedTasks[0]).to.eq(todos[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,14 +4,20 @@ import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
import buyGear from '../../../../website/common/script/ops/buy/buyGear';
|
||||
import {BuyMarketGearOperation} from '../../../../website/common/script/ops/buy/buyMarketGear';
|
||||
import shared from '../../../../website/common/script';
|
||||
import {
|
||||
BadRequest, NotAuthorized, NotFound,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyGear', () => {
|
||||
function buyGear (user, req, analytics) {
|
||||
let buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyMarketGear', () => {
|
||||
let user;
|
||||
let analytics = {track () {}};
|
||||
|
||||
@@ -111,6 +117,31 @@ describe('shared.ops.buyGear', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('does not buy equipment of different class', (done) => {
|
||||
user.stats.gp = 82;
|
||||
user.stats.class = 'warrior';
|
||||
|
||||
try {
|
||||
buyGear(user, {params: {key: 'weapon_special_winter2018Rogue'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not buy equipment in bulk', (done) => {
|
||||
user.stats.gp = 82;
|
||||
|
||||
try {
|
||||
buyGear(user, {params: {key: 'armor_warrior_1'}, quantity: 3});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotAbleToBuyInBulk'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO after user.ops.equip is done
|
||||
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
|
||||
user.stats.gp = 100;
|
||||
@@ -26,10 +26,11 @@ describe('shared.ops.releaseBoth', () => {
|
||||
|
||||
user.items.currentMount = animal;
|
||||
user.items.currentPet = animal;
|
||||
user.balance = 1.5;
|
||||
|
||||
user.achievements.triadBingo = true;
|
||||
});
|
||||
|
||||
it('returns an error when user balance is too low and user does not have triadBingo', (done) => {
|
||||
xit('returns an error when user balance is too low and user does not have triadBingo', (done) => {
|
||||
user.balance = 0;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
times,
|
||||
} from 'lodash';
|
||||
import Bluebird from 'bluebird';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { ApiUser, ApiGroup, ApiChallenge } from '../api-classes';
|
||||
import { requester } from '../requester';
|
||||
@@ -106,7 +105,7 @@ export async function createAndPopulateGroup (settings = {}) {
|
||||
guild: { guilds: [group._id] },
|
||||
};
|
||||
|
||||
let members = await Bluebird.all(
|
||||
let members = await Promise.all(
|
||||
times(numberOfMembers, () => {
|
||||
return generateUser(groupMembershipTypes[group.type]);
|
||||
})
|
||||
@@ -114,7 +113,7 @@ export async function createAndPopulateGroup (settings = {}) {
|
||||
|
||||
await group.update({ memberCount: numberOfMembers + 1});
|
||||
|
||||
let invitees = await Bluebird.all(
|
||||
let invitees = await Promise.all(
|
||||
times(numberOfInvites, () => {
|
||||
return generateUser();
|
||||
})
|
||||
@@ -126,9 +125,9 @@ export async function createAndPopulateGroup (settings = {}) {
|
||||
});
|
||||
});
|
||||
|
||||
await Bluebird.all(invitationPromises);
|
||||
await Promise.all(invitationPromises);
|
||||
|
||||
await Bluebird.map(invitees, (invitee) => invitee.sync());
|
||||
await Promise.all(invitees.map((invitee) => invitee.sync()));
|
||||
|
||||
return {
|
||||
groupLeader,
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
/* eslint-disable global-require */
|
||||
/* eslint-disable no-process-env */
|
||||
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
//------------------------------
|
||||
// Global modules
|
||||
//------------------------------
|
||||
@@ -16,7 +14,6 @@ global.sinon = require('sinon');
|
||||
let sinonStubPromise = require('sinon-stub-promise');
|
||||
sinonStubPromise(global.sinon);
|
||||
global.sandbox = sinon.sandbox.create();
|
||||
global.Promise = Bluebird;
|
||||
|
||||
import setupNconf from '../../website/server/libs/setupNconf';
|
||||
setupNconf('./config.json.example');
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
export async function sleep (seconds = 1) {
|
||||
let milliseconds = seconds * 1000;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, milliseconds);
|
||||
});
|
||||
}
|
||||
export { default as sleep } from '../../website/server/libs/sleep';
|
||||
@@ -1,13 +1,11 @@
|
||||
/* eslint-disable no-process-env */
|
||||
import nconf from 'nconf';
|
||||
import mongoose from 'mongoose';
|
||||
import Bluebird from 'bluebird';
|
||||
import setupNconf from '../../website/server/libs/setupNconf';
|
||||
|
||||
if (process.env.LOAD_SERVER === '0') { // when the server is in a different process we simply connect to mongoose
|
||||
setupNconf('./config.json');
|
||||
// Use Q promises instead of mpromise in mongoose
|
||||
mongoose.Promise = Bluebird;
|
||||
mongoose.connect(nconf.get('TEST_DB_URI'));
|
||||
} else { // When running tests and the server in the same process
|
||||
setupNconf('./config.json.example');
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
--timeout 8000
|
||||
--check-leaks
|
||||
--globals io
|
||||
-r babel-polyfill
|
||||
--require babel-register
|
||||
--require ./test/helpers/globals.helper
|
||||
--exit
|
||||
|
||||
@@ -11,12 +11,12 @@ source /home/vagrant/.profile
|
||||
|
||||
echo Setting up node...
|
||||
cd /vagrant
|
||||
nvm install
|
||||
nvm use
|
||||
nvm install 8
|
||||
nvm use 8
|
||||
nvm alias default current
|
||||
|
||||
echo Update npm...
|
||||
npm install -g npm@4
|
||||
npm install -g npm@5
|
||||
|
||||
echo Installing global modules...
|
||||
npm install -g gulp mocha node-pre-gyp
|
||||
|
||||
@@ -14,10 +14,17 @@ div
|
||||
router-view(v-if="!isUserLoggedIn || isStaticPage")
|
||||
template(v-else)
|
||||
template(v-if="isUserLoaded")
|
||||
div.resting-banner(v-if="showRestingBanner")
|
||||
span.content
|
||||
span.label {{ $t('innCheckOutBanner') }}
|
||||
span.separator |
|
||||
span.resume(@click="resumeDamage()") {{ $t('resumeDamage') }}
|
||||
div.closepadding(@click="hideBanner()")
|
||||
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close")
|
||||
notifications-display
|
||||
app-menu
|
||||
app-menu(:class='{"restingInn": showRestingBanner}')
|
||||
.container-fluid
|
||||
app-header
|
||||
app-header(:class='{"restingInn": showRestingBanner}')
|
||||
buyModal(
|
||||
:item="selectedItemToBuy || {}",
|
||||
:withPin="true",
|
||||
@@ -41,6 +48,8 @@ div
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
#loading-screen-inapp {
|
||||
#melior {
|
||||
margin: 0 auto;
|
||||
@@ -53,7 +62,7 @@ div
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
color: $white;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -72,10 +81,10 @@ div
|
||||
|
||||
.notification {
|
||||
border-radius: 1000px;
|
||||
background-color: #24cc8f;
|
||||
background-color: $green-10;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: .5em 1em;
|
||||
color: #fff;
|
||||
color: $white;
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
@@ -93,7 +102,9 @@ div
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
/* @TODO: The modal-open class is not being removed. Let's try this for now */
|
||||
.modal, .modal-open {
|
||||
overflow-y: scroll !important;
|
||||
@@ -101,13 +112,66 @@ div
|
||||
|
||||
.modal-backdrop.show {
|
||||
opacity: 1 !important;
|
||||
background-color: rgba(67, 40, 116, 0.9) !important;
|
||||
background-color: $purple-100 !important;
|
||||
}
|
||||
|
||||
/* Push progress bar above modals */
|
||||
#nprogress .bar {
|
||||
z-index: 1043 !important; /* Must stay above nav bar */
|
||||
}
|
||||
|
||||
.restingInn {
|
||||
.navbar {
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
#app-header {
|
||||
margin-top: 96px !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.resting-banner {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: $blue-10;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1030;
|
||||
display: flex;
|
||||
|
||||
.content {
|
||||
height: 24px;
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.closepadding {
|
||||
margin: 11px 24px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
|
||||
span svg path {
|
||||
stroke: $blue-500;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: $blue-100;
|
||||
margin: 0px 15px;
|
||||
}
|
||||
|
||||
.resume {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -128,6 +192,8 @@ import { setup as setupPayments } from 'client/libs/payments';
|
||||
import amazonPaymentsModal from 'client/components/payments/amazonModal';
|
||||
import spellsMixin from 'client/mixins/spells';
|
||||
|
||||
import svgClose from 'assets/svg/close.svg';
|
||||
|
||||
export default {
|
||||
mixins: [notifications, spellsMixin],
|
||||
name: 'app',
|
||||
@@ -143,6 +209,9 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: svgClose,
|
||||
}),
|
||||
selectedItemToBuy: null,
|
||||
selectedSpellToBuy: null,
|
||||
|
||||
@@ -152,6 +221,7 @@ export default {
|
||||
},
|
||||
loading: true,
|
||||
currentTipNumber: 0,
|
||||
bannerHidden: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -172,6 +242,9 @@ export default {
|
||||
|
||||
return this.$t(`tip${tipNumber}`);
|
||||
},
|
||||
showRestingBanner () {
|
||||
return !this.bannerHidden && this.user.preferences.sleep;
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('playSound', (sound) => {
|
||||
@@ -462,6 +535,12 @@ export default {
|
||||
hideLoadingScreen () {
|
||||
this.loading = false;
|
||||
},
|
||||
hideBanner () {
|
||||
this.bannerHidden = true;
|
||||
},
|
||||
resumeDamage () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
20
website/client/assets/scss/animals.scss
Normal file
20
website/client/assets/scss/animals.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.Pet {
|
||||
margin: auto !important;
|
||||
display: block;
|
||||
position: relative;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&:not([class*="FlyingPig"]) {
|
||||
top: -28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.Pet[class*="FlyingPig"] {
|
||||
top: 7px !important;
|
||||
}
|
||||
|
||||
.Pet.Pet-Dragon-Hydra {
|
||||
top: -16px !important;
|
||||
}
|
||||
@@ -33,3 +33,4 @@
|
||||
@import './banner';
|
||||
@import './progress-bar';
|
||||
@import './pin';
|
||||
@import './animals';
|
||||
|
||||
@@ -114,10 +114,9 @@
|
||||
.toggle-down(@click="sections.staff = !sections.staff", v-if="!sections.staff")
|
||||
.svg-icon(v-html="icons.downIcon")
|
||||
.section.row(v-if="sections.staff")
|
||||
// @TODO open member modal when clicking on a staff member
|
||||
.col-4.staff(v-for='user in staff', :class='{staff: user.type === "Staff", moderator: user.type === "Moderator", bailey: user.name === "It\'s Bailey"}')
|
||||
div
|
||||
.title {{user.name}}
|
||||
a.title(@click="viewStaffProfile(user.uuid)") {{user.name}}
|
||||
.svg-icon.staff-icon(v-html="icons.tierStaff", v-if='user.type === "Staff"')
|
||||
.svg-icon.mod-icon(v-html="icons.tierMod", v-if='user.type === "Moderator" && user.name !== "It\'s Bailey"')
|
||||
.svg-icon.npc-icon(v-html="icons.tierNPC", v-if='user.name === "It\'s Bailey"')
|
||||
@@ -595,78 +594,97 @@ export default {
|
||||
{
|
||||
name: 'beffymaroo',
|
||||
type: 'Staff',
|
||||
uuid: '9fe7183a-4b79-4c15-9629-a1aee3873390',
|
||||
},
|
||||
// {
|
||||
// name: 'lefnire',
|
||||
// type: 'Staff',
|
||||
// uuid: '00000000-0000-4000-9000-000000000000',
|
||||
// },
|
||||
{
|
||||
name: 'Lemoness',
|
||||
type: 'Staff',
|
||||
uuid: '7bde7864-ebc5-4ee2-a4b7-1070d464cdb0',
|
||||
},
|
||||
{
|
||||
name: 'paglias',
|
||||
type: 'Staff',
|
||||
uuid: 'ed4c688c-6652-4a92-9d03-a5a79844174a',
|
||||
},
|
||||
{
|
||||
name: 'redphoenix',
|
||||
type: 'Staff',
|
||||
uuid: 'cb46ad54-8c78-4dbc-a8ed-4e3185b2b3ff',
|
||||
},
|
||||
{
|
||||
name: 'SabreCat',
|
||||
type: 'Staff',
|
||||
uuid: '7f14ed62-5408-4e1b-be83-ada62d504931',
|
||||
},
|
||||
{
|
||||
name: 'TheHollidayInn',
|
||||
type: 'Staff',
|
||||
uuid: '206039c6-24e4-4b9f-8a31-61cbb9aa3f66',
|
||||
},
|
||||
{
|
||||
name: 'viirus',
|
||||
type: 'Staff',
|
||||
uuid: 'a327d7e0-1c2e-41be-9193-7b30b484413f',
|
||||
},
|
||||
{
|
||||
name: 'It\'s Bailey',
|
||||
type: 'Moderator',
|
||||
uuid: '9da65443-ed43-4c21-804f-d260c1361596',
|
||||
},
|
||||
{
|
||||
name: 'Alys',
|
||||
type: 'Moderator',
|
||||
uuid: 'd904bd62-da08-416b-a816-ba797c9ee265',
|
||||
},
|
||||
{
|
||||
name: 'Blade',
|
||||
type: 'Moderator',
|
||||
uuid: '75f270e8-c5db-4722-a5e6-a83f1b23f76b',
|
||||
},
|
||||
{
|
||||
name: 'Breadstrings',
|
||||
type: 'Moderator',
|
||||
uuid: '3b675c0e-d7a6-440c-8687-bc67cd0bf4e9',
|
||||
},
|
||||
{
|
||||
name: 'Cantras',
|
||||
type: 'Moderator',
|
||||
uuid: '28771972-ca6d-4c03-8261-e1734aa7d21d',
|
||||
},
|
||||
// {
|
||||
// name: 'Daniel the Bard',
|
||||
// type: 'Moderator',
|
||||
// uuid: '1f7c4a74-03a3-4b2c-b015-112d0acbd593',
|
||||
// },
|
||||
{
|
||||
name: 'deilann 5.0.5b',
|
||||
type: 'Moderator',
|
||||
uuid: 'e7b5d1e2-3b6e-4192-b867-8bafdb03eeec',
|
||||
},
|
||||
{
|
||||
name: 'Dewines',
|
||||
type: 'Moderator',
|
||||
uuid: '262a7afb-6b57-4d81-88e0-80d2e9f6cbdc',
|
||||
},
|
||||
{
|
||||
name: 'Fox_town',
|
||||
type: 'Moderator',
|
||||
uuid: 'a05f0152-d66b-4ef1-93ac-4adb195d0031',
|
||||
},
|
||||
{
|
||||
name: 'Megan',
|
||||
type: 'Moderator',
|
||||
uuid: '73e5125c-2c87-4004-8ccd-972aeac4f17a',
|
||||
},
|
||||
{
|
||||
name: 'shanaqui',
|
||||
type: 'Moderator',
|
||||
uuid: 'bb089388-28ae-4e42-a8fa-f0c2bfb6f779',
|
||||
},
|
||||
],
|
||||
newMessage: '',
|
||||
@@ -720,7 +738,6 @@ export default {
|
||||
this.newMessage = newText;
|
||||
},
|
||||
toggleSleep () {
|
||||
this.user.preferences.sleep = !this.user.preferences.sleep;
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
async sendMessage () {
|
||||
@@ -764,6 +781,14 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'world-boss-rage');
|
||||
}
|
||||
},
|
||||
|
||||
async viewStaffProfile (staffId) {
|
||||
let staffDetails = await this.$store.dispatch('members:fetchMember', { memberId: staffId });
|
||||
this.$root.$emit('habitica:show-profile', {
|
||||
user: staffDetails.data.data,
|
||||
startingPage: 'profile',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -193,31 +193,28 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import each from 'lodash/each';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import moment from 'moment';
|
||||
import Item from 'client/components/inventory/item';
|
||||
import ItemRows from 'client/components/ui/itemRows';
|
||||
import CountBadge from 'client/components/ui/countBadge';
|
||||
|
||||
import cardsModal from './cards-modal';
|
||||
|
||||
import HatchedPetDialog from '../stable/hatchedPetDialog';
|
||||
|
||||
import startQuestModal from '../../groups/startQuestModal';
|
||||
|
||||
import createAnimal from 'client/libs/createAnimal';
|
||||
|
||||
import QuestInfo from '../../shops/quests/questInfo.vue';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
const allowedSpecialItems = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
|
||||
import { mapState } from 'client/libs/store';
|
||||
import createAnimal from 'client/libs/createAnimal';
|
||||
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import DragDropDirective from 'client/directives/dragdrop.directive';
|
||||
import MouseMoveDirective from 'client/directives/mouseposition.directive';
|
||||
|
||||
const allowedSpecialItems = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
|
||||
|
||||
const groups = [
|
||||
['eggs', 'Pet_Egg_'],
|
||||
['hatchingPotions', 'Pet_HatchingPotion_'],
|
||||
@@ -321,14 +318,12 @@ export default {
|
||||
|
||||
let specialArray = itemsByType.special;
|
||||
|
||||
if (this.user.purchased.plan.customerId) {
|
||||
specialArray.push({
|
||||
key: 'mysteryItem',
|
||||
class: `inventory_present inventory_present_${moment().format('MM')}`,
|
||||
text: this.$t('subscriberItemText'),
|
||||
quantity: this.user.purchased.plan.mysteryItems.length,
|
||||
});
|
||||
}
|
||||
|
||||
for (let type in this.content.cardTypes) {
|
||||
let card = this.user.items.special[`${type}Received`] || [];
|
||||
|
||||
@@ -276,18 +276,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.stable .item .item-content.Pet:not(.FlyingPig) {
|
||||
top: -28px;
|
||||
}
|
||||
|
||||
.stable .item .item-content.FlyingPig {
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
.stable .item .item-content.Pet-Dragon-Hydra {
|
||||
top: -16px !important;
|
||||
}
|
||||
|
||||
.hatchablePopover {
|
||||
width: 180px
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@
|
||||
|
||||
hr
|
||||
|
||||
button.btn.btn-primary(@click='showBailey()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('showBaileyPop')") {{ $t('showBailey') }}
|
||||
button.btn.btn-primary(@click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('fixValPop')") {{ $t('fixVal') }}
|
||||
button.btn.btn-primary(v-if='user.preferences.disableClasses == true', @click='changeClassForUser(false)',
|
||||
button.btn.btn-primary.mr-2.mb-2(@click='showBailey()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('showBaileyPop')") {{ $t('showBailey') }}
|
||||
button.btn.btn-primary.mr-2.mb-2(@click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('fixValPop')") {{ $t('fixVal') }}
|
||||
button.btn.btn-primary.mb-2(v-if='user.preferences.disableClasses == true', @click='changeClassForUser(false)',
|
||||
popover-trigger='mouseenter', popover-placement='right', :popover="$t('enableClassPop')") {{ $t('enableClass') }}
|
||||
|
||||
hr
|
||||
@@ -93,7 +93,7 @@
|
||||
option(v-for='option in dayStartOptions' :value='option.value') {{option.name}}
|
||||
|
||||
.col-5
|
||||
button.btn.btn-block.btn-primary(@click='openDayStartModal()',
|
||||
button.btn.btn-block.btn-primary.mt-1(@click='openDayStartModal()',
|
||||
:disabled='newDayStart === user.preferences.dayStart')
|
||||
| {{ $t('saveCustomDayStart') }}
|
||||
hr
|
||||
@@ -111,8 +111,8 @@
|
||||
div
|
||||
ul.list-inline
|
||||
li(v-for='network in SOCIAL_AUTH_NETWORKS')
|
||||
button.btn.btn-primary(v-if='!user.auth[network.key].id', @click='socialAuth(network.key, user)') {{ $t('registerWithSocial', {network: network.name}) }}
|
||||
button.btn.btn-primary(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
|
||||
button.btn.btn-primary.mb-2(v-if='!user.auth[network.key].id', @click='socialAuth(network.key, user)') {{ $t('registerWithSocial', {network: network.name}) }}
|
||||
button.btn.btn-primary.mb-2(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
|
||||
button.btn.btn-danger(@click='deleteSocialAuth(network.key)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
|
||||
hr
|
||||
div(v-if='!user.auth.local.username')
|
||||
@@ -171,9 +171,9 @@
|
||||
div
|
||||
h5 {{ $t('dangerZone') }}
|
||||
div
|
||||
button.btn.btn-danger(@click='openResetModal()',
|
||||
button.btn.btn-danger.mr-2.mb-2(@click='openResetModal()',
|
||||
popover-trigger='mouseenter', popover-placement='right', v-b-popover.hover.auto="$t('resetAccPop')") {{ $t('resetAccount') }}
|
||||
button.btn.btn-danger(@click='openDeleteModal()',
|
||||
button.btn.btn-danger.mb-2(@click='openDeleteModal()',
|
||||
popover-trigger='mouseenter', v-b-popover.hover.auto="$t('deleteAccPop')") {{ $t('deleteAccount') }}
|
||||
</template>
|
||||
|
||||
|
||||
@@ -486,6 +486,10 @@
|
||||
return c.identifier === 'spells';
|
||||
})[0];
|
||||
|
||||
let questsCategory = _filter(categories, (c) => {
|
||||
return c.identifier === 'quests';
|
||||
})[0];
|
||||
|
||||
let setCategories = _filter(categories, 'specialClass');
|
||||
|
||||
let result = _groupBy(setCategories, 'specialClass');
|
||||
@@ -496,6 +500,12 @@
|
||||
];
|
||||
}
|
||||
|
||||
if (questsCategory) {
|
||||
result.quests = [
|
||||
questsCategory,
|
||||
];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
isGearLocked (gear) {
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
v-if='type === "reward"')
|
||||
.d-flex
|
||||
h2.tasks-column-title
|
||||
| {{ $t(types[type].label) }}
|
||||
| {{ $t(typeLabel) }}
|
||||
.badge.badge-pill.badge-purple.column-badge(v-if="badgeCount > 0") {{ badgeCount }}
|
||||
.filters.d-flex.justify-content-end
|
||||
.filter.small-text(
|
||||
v-for="filter in types[type].filters",
|
||||
:class="{active: activeFilters[type].label === filter.label}",
|
||||
v-for="filter in typeFilters",
|
||||
:class="{active: activeFilter.label === filter}",
|
||||
@click="activateFilter(type, filter)",
|
||||
) {{ $t(filter.label) }}
|
||||
) {{ $t(filter) }}
|
||||
.tasks-list(ref="tasksWrapper")
|
||||
textarea.quick-add(
|
||||
:rows="quickAddRows",
|
||||
@@ -26,19 +26,19 @@
|
||||
)
|
||||
transition(name="quick-add-tip-slide")
|
||||
.quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')")
|
||||
clear-completed-todos(v-if="activeFilters[type].label === 'complete2'")
|
||||
clear-completed-todos(v-if="activeFilter.label === 'complete2' && isUser === true")
|
||||
.column-background(
|
||||
v-if="isUser === true",
|
||||
:class="{'initial-description': initialColumnDescription}",
|
||||
ref="columnBackground",
|
||||
)
|
||||
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
|
||||
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}}
|
||||
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(typeLabel)})}}
|
||||
.small-text {{$t(`${type}sDesc`)}}
|
||||
draggable.sortable-tasks(
|
||||
ref="tasksList",
|
||||
@update='sorted',
|
||||
:options='{disabled: activeFilters[type].label === "scheduled"}',
|
||||
:options='{disabled: activeFilter.label === "scheduled"}',
|
||||
)
|
||||
task(
|
||||
v-for="task in taskList",
|
||||
@@ -245,10 +245,10 @@
|
||||
<script>
|
||||
import Task from './task';
|
||||
import ClearCompletedTodos from './clearCompletedTodos';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import throttle from 'lodash/throttle';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import buyMixin from 'client/mixins/buy';
|
||||
import { mapState, mapActions } from 'client/libs/store';
|
||||
import { mapState, mapActions, mapGetters } from 'client/libs/store';
|
||||
import shopItem from '../shops/shopItem';
|
||||
import BuyQuestModal from 'client/components/shops/quests/buyQuestModal.vue';
|
||||
|
||||
@@ -258,6 +258,12 @@ import inAppRewards from 'common/script/libs/inAppRewards';
|
||||
import spells from 'common/script/content/spells';
|
||||
import taskDefaults from 'common/script/libs/taskDefaults';
|
||||
|
||||
import {
|
||||
getTypeLabel,
|
||||
getFilterLabels,
|
||||
getActiveFilter,
|
||||
} from 'client/libs/store/helpers/filterTasks.js';
|
||||
|
||||
import svgPin from 'assets/svg/pin.svg';
|
||||
import habitIcon from 'assets/svg/habit.svg';
|
||||
import dailyIcon from 'assets/svg/daily.svg';
|
||||
@@ -274,44 +280,21 @@ export default {
|
||||
shopItem,
|
||||
draggable,
|
||||
},
|
||||
props: ['type', 'isUser', 'searchText', 'selectedTags', 'taskListOverride', 'group'], // @TODO: maybe we should store the group on state?
|
||||
// Set default values for props
|
||||
// allows for better control of props values
|
||||
// allows for better control of where this component is called
|
||||
props: {
|
||||
type: {},
|
||||
isUser: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchText: {},
|
||||
selectedTags: {},
|
||||
taskListOverride: {},
|
||||
group: {},
|
||||
}, // @TODO: maybe we should store the group on state?
|
||||
data () {
|
||||
// @TODO refactor this so that filter functions aren't in data
|
||||
const types = Object.freeze({
|
||||
habit: {
|
||||
label: 'habits',
|
||||
filters: [
|
||||
{label: 'all', filter: () => true, default: true},
|
||||
{label: 'yellowred', filter: t => t.value < 1}, // weak
|
||||
{label: 'greenblue', filter: t => t.value >= 1}, // strong
|
||||
],
|
||||
},
|
||||
daily: {
|
||||
label: 'dailies',
|
||||
filters: [
|
||||
{label: 'all', filter: () => true, default: true},
|
||||
{label: 'due', filter: t => !t.completed && shouldDo(new Date(), t, this.userPreferences)},
|
||||
{label: 'notDue', filter: t => t.completed || !shouldDo(new Date(), t, this.userPreferences)},
|
||||
],
|
||||
},
|
||||
todo: {
|
||||
label: 'todos',
|
||||
filters: [
|
||||
{label: 'remaining', filter: t => !t.completed, default: true}, // active
|
||||
{label: 'scheduled', filter: t => !t.completed && t.date, sort: t => t.date},
|
||||
{label: 'complete2', filter: t => t.completed},
|
||||
],
|
||||
},
|
||||
reward: {
|
||||
label: 'rewards',
|
||||
filters: [
|
||||
{label: 'all', filter: () => true, default: true},
|
||||
{label: 'custom', filter: () => true}, // all rewards made by the user
|
||||
{label: 'wishlist', filter: () => false}, // not user tasks
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const icons = Object.freeze({
|
||||
habit: habitIcon,
|
||||
daily: dailyIcon,
|
||||
@@ -320,14 +303,15 @@ export default {
|
||||
pin: svgPin,
|
||||
});
|
||||
|
||||
let activeFilters = {};
|
||||
for (let type in types) {
|
||||
activeFilters[type] = types[type].filters.find(f => f.default === true);
|
||||
}
|
||||
let typeLabel = '';
|
||||
let typeFilters = [];
|
||||
let activeFilter = {};
|
||||
|
||||
return {
|
||||
types,
|
||||
activeFilters,
|
||||
typeLabel,
|
||||
typeFilters,
|
||||
activeFilter,
|
||||
|
||||
icons,
|
||||
openedCompletedTodos: false,
|
||||
|
||||
@@ -339,45 +323,37 @@ export default {
|
||||
selectedItemToBuy: {},
|
||||
};
|
||||
},
|
||||
created () {
|
||||
// Set Task Column Label
|
||||
this.typeLabel = getTypeLabel(this.type);
|
||||
// Get Category Filter Labels
|
||||
this.typeFilters = getFilterLabels(this.type);
|
||||
// Set default filter for task column
|
||||
this.activateFilter(this.type);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
tasks: 'tasks.data',
|
||||
user: 'user.data',
|
||||
userPreferences: 'user.data.preferences',
|
||||
}),
|
||||
onUserPage () {
|
||||
let onUserPage = Boolean(this.taskList.length) && (!this.taskListOverride || this.taskListOverride.length === 0);
|
||||
|
||||
if (!onUserPage) {
|
||||
this.activateFilter('daily', this.types.daily.filters[0]);
|
||||
this.types.reward.filters = [];
|
||||
}
|
||||
|
||||
return onUserPage;
|
||||
},
|
||||
...mapGetters({
|
||||
getFilteredTaskList: 'tasks:getFilteredTaskList',
|
||||
getUnfilteredTaskList: 'tasks:getUnfilteredTaskList',
|
||||
getUserPreferences: 'user:preferences',
|
||||
getUserBuffs: 'user:buffs',
|
||||
}),
|
||||
taskList () {
|
||||
// @TODO: This should not default to user's tasks. It should require that you pass options in
|
||||
const filter = this.activeFilters[this.type];
|
||||
let filteredTaskList = this.isUser ?
|
||||
this.getFilteredTaskList({
|
||||
type: this.type,
|
||||
filterType: this.activeFilter.label,
|
||||
}) :
|
||||
this.taskListOverride;
|
||||
|
||||
let taskList = this.tasks[`${this.type}s`];
|
||||
if (this.taskListOverride) taskList = this.taskListOverride;
|
||||
let taggedList = this.filterByTagList(filteredTaskList, this.selectedTags);
|
||||
let searchedList = this.filterBySearchText(taggedList, this.searchText);
|
||||
|
||||
if (taskList.length > 0 && ['scheduled', 'due'].indexOf(filter.label) === -1) {
|
||||
let taskListSorted = this.$store.dispatch('tasks:order', [
|
||||
taskList,
|
||||
this.user.tasksOrder,
|
||||
]);
|
||||
|
||||
taskList = taskListSorted[`${this.type}s`];
|
||||
}
|
||||
|
||||
if (filter.sort) {
|
||||
taskList = sortBy(taskList, filter.sort);
|
||||
}
|
||||
|
||||
return taskList.filter(t => {
|
||||
return this.filterTask(t);
|
||||
});
|
||||
return searchedList;
|
||||
},
|
||||
inAppRewards () {
|
||||
let watchRefresh = this.forceRefresh; // eslint-disable-line
|
||||
@@ -393,7 +369,7 @@ export default {
|
||||
};
|
||||
|
||||
for (let key in seasonalSkills) {
|
||||
if (this.user.stats.buffs[key]) {
|
||||
if (this.getUserBuffs(key)) {
|
||||
let debuff = seasonalSkills[key];
|
||||
let item = Object.assign({}, spells.special[debuff]);
|
||||
item.text = item.text();
|
||||
@@ -406,7 +382,7 @@ export default {
|
||||
return rewards;
|
||||
},
|
||||
hasRewardsList () {
|
||||
return this.isUser === true && this.type === 'reward' && this.activeFilters[this.type].label !== 'custom';
|
||||
return this.isUser === true && this.type === 'reward' && this.activeFilter.label !== 'custom';
|
||||
},
|
||||
initialColumnDescription () {
|
||||
// Show the column description in the middle only if there are no elements (tasks or in app items)
|
||||
@@ -414,14 +390,7 @@ export default {
|
||||
if (this.inAppRewards && this.inAppRewards.length >= 0) return false;
|
||||
}
|
||||
|
||||
return this.tasks[`${this.type}s`].length === 0;
|
||||
},
|
||||
dailyDueDefaultView () {
|
||||
if (this.user.preferences.dailyDueDefaultView) {
|
||||
this.activateFilter('daily', this.types.daily.filters[1]);
|
||||
}
|
||||
|
||||
return this.user.preferences.dailyDueDefaultView;
|
||||
return this.taskList.length === 0;
|
||||
},
|
||||
quickAddPlaceholder () {
|
||||
const type = this.$t(this.type);
|
||||
@@ -431,16 +400,14 @@ export default {
|
||||
// 0 means the badge will not be shown
|
||||
// It is shown for the all and due views of dailies
|
||||
// and for the active and scheduled views of todos.
|
||||
if (this.type === 'todo') {
|
||||
if (this.activeFilters.todo.label !== 'complete2') return this.taskList.length;
|
||||
} else if (this.type === 'daily') {
|
||||
const activeFilter = this.activeFilters.daily.label;
|
||||
|
||||
if (activeFilter === 'due') {
|
||||
if (this.type === 'todo' && this.activeFilter.label !== 'complete2') {
|
||||
return this.taskList.length;
|
||||
} else if (activeFilter === 'all') {
|
||||
} else if (this.type === 'daily') {
|
||||
if (this.activeFilter.label === 'due') {
|
||||
return this.taskList.length;
|
||||
} else if (this.activeFilter.label === 'all') {
|
||||
return this.taskList.reduce((count, t) => {
|
||||
return !t.completed && shouldDo(new Date(), t, this.userPreferences) ? count + 1 : count;
|
||||
return !t.completed && shouldDo(new Date(), t, this.getUserPreferences) ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -455,10 +422,6 @@ export default {
|
||||
}, 250),
|
||||
deep: true,
|
||||
},
|
||||
dailyDueDefaultView () {
|
||||
if (!this.dailyDueDefaultView) return;
|
||||
this.activateFilter('daily', this.types.daily.filters[1]);
|
||||
},
|
||||
quickAddFocused (newValue) {
|
||||
if (newValue) this.quickAddRows = this.quickAddText.split('\n').length;
|
||||
if (!newValue) this.quickAddRows = 1;
|
||||
@@ -491,7 +454,7 @@ export default {
|
||||
const filteredList = this.taskList;
|
||||
const taskToMove = filteredList[data.oldIndex];
|
||||
const taskIdToMove = taskToMove._id;
|
||||
let originTasks = this.tasks[`${this.type}s`];
|
||||
let originTasks = this.getUnfilteredTaskList(this.type);
|
||||
if (this.taskListOverride) originTasks = this.taskListOverride;
|
||||
|
||||
// Server
|
||||
@@ -518,7 +481,7 @@ export default {
|
||||
},
|
||||
async moveTo (task, where) { // where is 'top' or 'bottom'
|
||||
const taskIdToMove = task._id;
|
||||
const list = this.tasks[`${this.type}s`];
|
||||
const list = this.getUnfilteredTaskList(this.type);
|
||||
|
||||
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
|
||||
const moved = list.splice(oldPosition, 1);
|
||||
@@ -551,19 +514,27 @@ export default {
|
||||
return task;
|
||||
});
|
||||
|
||||
this.quickAddText = null;
|
||||
this.quickAddText = '';
|
||||
this.quickAddRows = 1;
|
||||
this.createTask(tasks);
|
||||
},
|
||||
editTask (task) {
|
||||
this.$emit('editTask', task);
|
||||
},
|
||||
activateFilter (type, filter) {
|
||||
if (type === 'todo' && filter.label === 'complete2') {
|
||||
activateFilter (type, filter = '') {
|
||||
// Needs a separate API call as this data may not reside in store
|
||||
if (type === 'todo' && filter === 'complete2') {
|
||||
this.loadCompletedTodos();
|
||||
}
|
||||
|
||||
this.activeFilters[type] = filter;
|
||||
// the only time activateFilter is called with filter==='' is when the component is first created
|
||||
// this can be used to check If the user has set 'due' as default filter for daily
|
||||
// and set the filter as 'due' only when the component first loads and not on subsequent reloads.
|
||||
if (type === 'daily' && filter === '' && this.user.preferences.dailyDueDefaultView) {
|
||||
filter = 'due';
|
||||
}
|
||||
|
||||
this.activeFilter = getActiveFilter(type, filter);
|
||||
},
|
||||
setColumnBackgroundVisibility () {
|
||||
this.$nextTick(() => {
|
||||
@@ -591,35 +562,36 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
filterTask (task) {
|
||||
// View
|
||||
if (!this.activeFilters[task.type].filter(task)) return false;
|
||||
|
||||
// Tags
|
||||
const selectedTags = this.selectedTags;
|
||||
|
||||
if (selectedTags && selectedTags.length > 0) {
|
||||
const hasAllSelectedTag = selectedTags.every(tagId => {
|
||||
return task.tags.indexOf(tagId) !== -1;
|
||||
});
|
||||
|
||||
if (!hasAllSelectedTag) return false;
|
||||
filterByTagList (taskList, tagList = []) {
|
||||
let filteredTaskList = taskList;
|
||||
// fitler requested tasks by tags
|
||||
if (!isEmpty(tagList)) {
|
||||
filteredTaskList = taskList.filter(
|
||||
task => tagList.every(tag => task.tags.indexOf(tag) !== -1)
|
||||
);
|
||||
}
|
||||
|
||||
// Text
|
||||
const searchText = this.searchText;
|
||||
|
||||
if (!searchText) return true;
|
||||
if (task.text.toLowerCase().indexOf(searchText) !== -1) return true;
|
||||
if (task.notes.toLowerCase().indexOf(searchText) !== -1) return true;
|
||||
|
||||
if (task.checklist && task.checklist.length) {
|
||||
const checklistItemIndex = task.checklist.findIndex(({text}) => {
|
||||
return text.toLowerCase().indexOf(searchText) !== -1;
|
||||
return filteredTaskList;
|
||||
},
|
||||
filterBySearchText (taskList, searchText = '') {
|
||||
let filteredTaskList = taskList;
|
||||
// filter requested tasks by search text
|
||||
if (searchText) {
|
||||
// to ensure broadest case insensitive search matching
|
||||
let searchTextLowerCase = searchText.toLowerCase();
|
||||
filteredTaskList = taskList.filter(
|
||||
task => {
|
||||
// eslint rule disabled for block to allow nested binary expression
|
||||
/* eslint-disable no-extra-parens */
|
||||
return (
|
||||
task.text.toLowerCase().indexOf(searchTextLowerCase) > -1 ||
|
||||
(task.note && task.note.toLowerCase().indexOf(searchTextLowerCase) > -1) ||
|
||||
(task.checklist && task.checklist.length > 0 &&
|
||||
task.checklist.some(checkItem => checkItem.text.toLowerCase().indexOf(searchTextLowerCase) > -1))
|
||||
);
|
||||
/* eslint-enable no-extra-parens */
|
||||
});
|
||||
|
||||
return checklistItemIndex !== -1;
|
||||
}
|
||||
return filteredTaskList;
|
||||
},
|
||||
openBuyDialog (rewardItem) {
|
||||
if (rewardItem.locked) return;
|
||||
|
||||
@@ -726,6 +726,11 @@ export default {
|
||||
// @TODO: This whole component is mutating a prop and that causes issues. We need to not copy the prop similar to group modals
|
||||
if (this.task) this.checklist = clone(this.task.checklist);
|
||||
},
|
||||
'task.startDate' () {
|
||||
if (this.task && this.repeatsOn) {
|
||||
this.calculateMonthlyRepeatDays(this.repeatsOn);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -792,23 +797,7 @@ export default {
|
||||
return repeatsOn;
|
||||
},
|
||||
set (newValue) {
|
||||
const task = this.task;
|
||||
|
||||
if (task.frequency === 'monthly' && newValue === 'dayOfMonth') {
|
||||
const date = moment(task.startDate).date();
|
||||
task.weeksOfMonth = [];
|
||||
task.daysOfMonth = [date];
|
||||
} else if (task.frequency === 'monthly' && newValue === 'dayOfWeek') {
|
||||
const week = Math.ceil(moment(task.startDate).date() / 7) - 1;
|
||||
const dayOfWeek = moment(task.startDate).day();
|
||||
const shortDay = this.dayMapping[dayOfWeek];
|
||||
task.daysOfMonth = [];
|
||||
task.weeksOfMonth = [week];
|
||||
for (let key in task.repeat) {
|
||||
task.repeat[key] = false;
|
||||
}
|
||||
task.repeat[shortDay] = true;
|
||||
}
|
||||
this.calculateMonthlyRepeatDays(newValue);
|
||||
},
|
||||
},
|
||||
selectedTags () {
|
||||
@@ -870,6 +859,27 @@ export default {
|
||||
weekdaysMin (dayNumber) {
|
||||
return moment.weekdaysMin(dayNumber);
|
||||
},
|
||||
calculateMonthlyRepeatDays (repeatsOn) {
|
||||
const task = this.task;
|
||||
|
||||
if (task.frequency === 'monthly') {
|
||||
if (repeatsOn === 'dayOfMonth') {
|
||||
const date = moment(task.startDate).date();
|
||||
task.weeksOfMonth = [];
|
||||
task.daysOfMonth = [date];
|
||||
} else if (repeatsOn === 'dayOfWeek') {
|
||||
const week = Math.ceil(moment(task.startDate).date() / 7) - 1;
|
||||
const dayOfWeek = moment(task.startDate).day();
|
||||
const shortDay = this.dayMapping[dayOfWeek];
|
||||
task.daysOfMonth = [];
|
||||
task.weeksOfMonth = [week];
|
||||
for (let key in task.repeat) {
|
||||
task.repeat[key] = false;
|
||||
}
|
||||
task.repeat[shortDay] = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
if (this.newChecklistItem) this.addChecklistItem();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ div
|
||||
.header
|
||||
h1 {{user.profile.name}}
|
||||
h4
|
||||
strong {{ $t('userId') }}:
|
||||
strong {{ $t('userId') }}:
|
||||
| {{user._id}}
|
||||
.col-12.col-md-4
|
||||
button.btn.btn-secondary(v-if='user._id === userLoggedIn._id', @click='editing = !editing') {{ $t('edit') }}
|
||||
@@ -131,10 +131,10 @@ div
|
||||
)
|
||||
div(:class="`shop_${equippedItems[key]}`")
|
||||
b-popover(
|
||||
v-if="label !== 'skip' && equippedItems[key] && equippedItems[key].indexOf(\"base_0\") === -1",
|
||||
v-if="label !== 'skip' && equippedItems[key] && equippedItems[key].indexOf('base_0') === -1",
|
||||
:target="key",
|
||||
triggers="hover",
|
||||
:placement="'right'",
|
||||
:placement="'bottom'",
|
||||
:preventOverflow="false",
|
||||
)
|
||||
h4.gearTitle {{ getGearTitle(equippedItems[key]) }}
|
||||
@@ -146,42 +146,35 @@ div
|
||||
.col-12.col-md-6
|
||||
h2.text-center {{$t('costume')}}
|
||||
.well
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.eyewear && costumeItems.eyewear.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.eyewear}`")
|
||||
h3 {{$t('eyewear')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.head && costumeItems.head.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.head}`")
|
||||
h3 {{$t('headgearCapitalized')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.headAccessory && costumeItems.headAccessory.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.headAccessory}`")
|
||||
h3 {{$t('headAccess')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.back && costumeItems.back.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.back}`")
|
||||
h3 {{$t('backAccess')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.armor && costumeItems.armor.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.armor}`")
|
||||
h3 {{$t('armorCapitalized')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.body && costumeItems.body.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.body}`")
|
||||
h3 {{$t('bodyAccess')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.weapon && costumeItems.weapon.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.weapon}`")
|
||||
h3 {{$t('mainHand')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: user.preferences.background}', style="overflow:hidden")
|
||||
// Use similar for loop for costume items, except show background if label is 'skip'.
|
||||
.col-12.col-md-4.item-wrapper(v-for="(label, key) in equipTypes")
|
||||
// Append a "C" to the key name since HTML IDs have to be unique.
|
||||
.box(
|
||||
:id="key + 'C'",
|
||||
v-if="label !== 'skip'",
|
||||
:class='{white: costumeItems[key] && costumeItems[key].indexOf("base_0") === -1}'
|
||||
)
|
||||
div(:class="`shop_${costumeItems[key]}`")
|
||||
// Show background on 8th tile rather than a piece of equipment.
|
||||
.box(v-if="label === 'skip'",
|
||||
:class='{white: user.preferences.background}', style="overflow:hidden"
|
||||
)
|
||||
div(:class="'icon_background_' + user.preferences.background")
|
||||
h3 {{$t('background')}}
|
||||
.col-12.col-md-4.item-wrapper
|
||||
.box(:class='{white: costumeItems.shield && costumeItems.shield.indexOf("base_0") === -1}')
|
||||
div(:class="`shop_${costumeItems.shield}`")
|
||||
h3 {{$t('offHand')}}
|
||||
b-popover(
|
||||
v-if="label !== 'skip' && costumeItems[key] && costumeItems[key].indexOf('base_0') === -1",
|
||||
:target="key + 'C'",
|
||||
triggers="hover",
|
||||
:placement="'bottom'",
|
||||
:preventOverflow="false",
|
||||
)
|
||||
h4.gearTitle {{ getGearTitle(costumeItems[key]) }}
|
||||
attributesGrid.attributesGrid(
|
||||
:item="content.gear.flat[costumeItems[key]]",
|
||||
)
|
||||
|
||||
h3(v-if="label !== 'skip'") {{ label }}
|
||||
h3(v-else) {{ $t('background') }}
|
||||
|
||||
.row.pet-mount-row
|
||||
.col-12.col-md-6
|
||||
h2.text-center(v-once) {{ $t('pets') }}
|
||||
@@ -189,7 +182,7 @@ div
|
||||
.row.col-12
|
||||
.col-12.col-md-4
|
||||
.box(:class='{white: user.items.currentPet}')
|
||||
.pet(:class="`Pet-${user.items.currentPet}`")
|
||||
.Pet(:class="`Pet-${user.items.currentPet}`")
|
||||
.col-12.col-md-8
|
||||
div
|
||||
| {{ formatAnimal(user.items.currentPet, 'pet') }}
|
||||
@@ -224,7 +217,7 @@ div
|
||||
span.hint(:popover-title='$t(statInfo.title)', popover-placement='right',
|
||||
:popover='$t(statInfo.popover)', popover-trigger='mouseenter')
|
||||
.stat-title(:class='stat') {{ $t(statInfo.title) }}
|
||||
strong.number {{ statsComputed[stat] }}
|
||||
strong.number {{ statsComputed[stat] | floorWholeNumber }}
|
||||
.col-12.col-md-6
|
||||
ul.bonus-stats
|
||||
li
|
||||
@@ -340,10 +333,6 @@ div
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.pet {
|
||||
margin-top: -1.4em !important;
|
||||
}
|
||||
|
||||
.mount {
|
||||
margin-top: -0.2em !important;
|
||||
}
|
||||
@@ -574,6 +563,7 @@ import each from 'lodash/each';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import size from 'lodash/size';
|
||||
import keys from 'lodash/keys';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { beastMasterProgress, mountMasterProgress } from '../../../common/script/count';
|
||||
import statsComputed from '../../../common/script/libs/statsComputed';
|
||||
import autoAllocate from '../../../common/script/fns/autoAllocate';
|
||||
@@ -786,10 +776,13 @@ export default {
|
||||
save () {
|
||||
let values = {};
|
||||
|
||||
each(this.editingProfile, (value, key) => {
|
||||
let edits = cloneDeep(this.editingProfile);
|
||||
|
||||
each(edits, (value, key) => {
|
||||
// Using toString because we need to compare two arrays (websites)
|
||||
let curVal = this.user.profile[key];
|
||||
if (!curVal || this.editingProfile[key].toString() !== curVal.toString()) {
|
||||
|
||||
if (!curVal || value.toString() !== curVal.toString()) {
|
||||
values[`profile.${key}`] = value;
|
||||
this.$set(this.user.profile, key, value);
|
||||
}
|
||||
|
||||
3
website/client/filters/floorWholeNumber.js
Normal file
3
website/client/filters/floorWholeNumber.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function floorWholeNumber (val) {
|
||||
return Math.floor(val);
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import Vue from 'vue';
|
||||
import round from './round';
|
||||
import floor from './floor';
|
||||
import roundBigNumber from './roundBigNumber';
|
||||
import floorWholeNumber from './floorWholeNumber';
|
||||
|
||||
Vue.filter('round', round);
|
||||
Vue.filter('floor', floor);
|
||||
Vue.filter('roundBigNumber', roundBigNumber);
|
||||
Vue.filter('floorWholeNumber', floorWholeNumber);
|
||||
68
website/client/libs/store/helpers/filterTasks.js
Normal file
68
website/client/libs/store/helpers/filterTasks.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { shouldDo } from 'common/script/cron';
|
||||
|
||||
// Task filter data
|
||||
// @TODO find a way to include user preferences w.r.t sort and defaults
|
||||
const taskFilters = {
|
||||
habit: {
|
||||
label: 'habits',
|
||||
filters: [
|
||||
{ label: 'all', filterFn: () => true, default: true },
|
||||
{ label: 'yellowred', filterFn: t => t.value < 1 }, // weak
|
||||
{ label: 'greenblue', filterFn: t => t.value >= 1 }, // strong
|
||||
],
|
||||
},
|
||||
daily: {
|
||||
label: 'dailies',
|
||||
filters: [
|
||||
{ label: 'all', filterFn: () => true, default: true },
|
||||
{ label: 'due', filterFn: userPrefs => t => !t.completed && shouldDo(new Date(), t, userPrefs) },
|
||||
{ label: 'notDue', filterFn: userPrefs => t => t.completed || !shouldDo(new Date(), t, userPrefs) },
|
||||
],
|
||||
},
|
||||
todo: {
|
||||
label: 'todos',
|
||||
filters: [
|
||||
{ label: 'remaining', filterFn: t => !t.completed, default: true }, // active
|
||||
{ label: 'scheduled', filterFn: t => !t.completed && t.date, sort: t => t.date },
|
||||
{ label: 'complete2', filterFn: t => t.completed },
|
||||
],
|
||||
},
|
||||
reward: {
|
||||
label: 'rewards',
|
||||
filters: [
|
||||
{ label: 'all', filterFn: () => true, default: true },
|
||||
{ label: 'custom', filterFn: () => true }, // all rewards made by the user
|
||||
{ label: 'wishlist', filterFn: () => false }, // not user tasks
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function typeLabel (filterList) {
|
||||
return (type) => filterList[type].label;
|
||||
}
|
||||
|
||||
export const getTypeLabel = typeLabel(taskFilters);
|
||||
|
||||
function filterLabel (filterList) {
|
||||
return (type) => {
|
||||
let filterListByType = filterList[type].filters;
|
||||
let filterListOfLabels = new Array(filterListByType.length);
|
||||
filterListByType.forEach(({ label }, i) => filterListOfLabels[i] = label);
|
||||
|
||||
return filterListOfLabels;
|
||||
};
|
||||
}
|
||||
|
||||
export const getFilterLabels = filterLabel(taskFilters);
|
||||
|
||||
function activeFilter (filterList) {
|
||||
return (type, filterType = '') => {
|
||||
let filterListByType = filterList[type].filters;
|
||||
if (filterType) {
|
||||
return filterListByType.find(f => f.label === filterType);
|
||||
}
|
||||
return filterListByType.find(f => f.default === true);
|
||||
};
|
||||
}
|
||||
|
||||
export const getActiveFilter = activeFilter(taskFilters);
|
||||
31
website/client/libs/store/helpers/orderTasks.js
Normal file
31
website/client/libs/store/helpers/orderTasks.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
// sets task order for single task type only.
|
||||
// Accepts task list and corresponding taskorder for its task type.
|
||||
export function orderSingleTypeTasks (rawTasks, taskOrder) {
|
||||
// if there is no predefined task order return task list as is.
|
||||
if (!taskOrder) return rawTasks;
|
||||
const orderedTasks = new Array(rawTasks.length);
|
||||
const unorderedTasks = []; // What we want to add later
|
||||
|
||||
rawTasks.forEach((task, index) => {
|
||||
const taskId = task._id;
|
||||
const i = taskOrder[index] === taskId ? index : taskOrder.indexOf(taskId);
|
||||
if (i === -1) {
|
||||
unorderedTasks.push(task);
|
||||
} else {
|
||||
orderedTasks[i] = task;
|
||||
}
|
||||
});
|
||||
|
||||
return compact(orderedTasks).concat(unorderedTasks);
|
||||
}
|
||||
|
||||
export function orderMultipleTypeTasks (rawTasks, tasksOrder) {
|
||||
return {
|
||||
habits: orderSingleTypeTasks(rawTasks.habits, tasksOrder.habits),
|
||||
dailys: orderSingleTypeTasks(rawTasks.dailys, tasksOrder.dailys),
|
||||
todos: orderSingleTypeTasks(rawTasks.todos, tasksOrder.todos),
|
||||
rewards: orderSingleTypeTasks(rawTasks.rewards, tasksOrder.rewards),
|
||||
};
|
||||
}
|
||||
@@ -55,7 +55,13 @@ export async function join (store, payload) {
|
||||
const user = store.state.user.data;
|
||||
const invitations = user.invitations;
|
||||
|
||||
let response = await axios.post(`/api/v3/groups/${groupId}/join`);
|
||||
let response;
|
||||
try {
|
||||
response = await axios.post(`/api/v3/groups/${groupId}/join`);
|
||||
} catch (err) {
|
||||
alert(err.response.data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'guild') {
|
||||
const invitationI = invitations.guilds.findIndex(i => i.id === groupId);
|
||||
|
||||
@@ -56,7 +56,11 @@ export async function set (store, changes) {
|
||||
// .catch((err) => console.error('set', err));
|
||||
}
|
||||
|
||||
export async function sleep () {
|
||||
export async function sleep (store) {
|
||||
const user = store.state.user.data;
|
||||
|
||||
user.preferences.sleep = !user.preferences.sleep;
|
||||
|
||||
let response = await axios.post('/api/v3/user/sleep');
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { shouldDo } from 'common/script/cron';
|
||||
|
||||
// Library / Utility function
|
||||
import { orderSingleTypeTasks } from 'client/libs/store/helpers/orderTasks.js';
|
||||
import { getActiveFilter } from 'client/libs/store/helpers/filterTasks.js';
|
||||
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
// Return all the tags belonging to an user task
|
||||
export function getTagsFor (store) {
|
||||
return (task) => {
|
||||
@@ -109,3 +115,47 @@ export function getTaskClasses (store) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Returns all list for given task type
|
||||
export function getUnfilteredTaskList ({state}) {
|
||||
return (type) => state.tasks.data[`${type}s`];
|
||||
}
|
||||
|
||||
// Returns filtered, sorted, ordered, tag filtered, and search filtered task list
|
||||
// @TODO: sort task list based on used preferences
|
||||
export function getFilteredTaskList ({state, getters}) {
|
||||
return ({
|
||||
type,
|
||||
filterType = '',
|
||||
}) => {
|
||||
// get requested tasks
|
||||
// check if task list has been passed as override props
|
||||
// assumption: type will always be passed as param
|
||||
let requestedTasks = getters['tasks:getUnfilteredTaskList'](type);
|
||||
|
||||
let userPreferences = state.user.data.preferences;
|
||||
let taskOrderForType = state.user.data.tasksOrder[type];
|
||||
|
||||
// order tasks based on user set task order
|
||||
// Still needs unit test for this..
|
||||
if (requestedTasks.length > 0 && ['scheduled', 'due'].indexOf(filterType.label) === -1) {
|
||||
requestedTasks = orderSingleTypeTasks(requestedTasks, taskOrderForType);
|
||||
}
|
||||
|
||||
let selectedFilter = getActiveFilter(type, filterType);
|
||||
// Pass user preferences to the filter function which uses currying
|
||||
if (type === 'daily' && (filterType === 'due' || filterType === 'notDue')) {
|
||||
selectedFilter = {
|
||||
...selectedFilter,
|
||||
filterFn: selectedFilter.filterFn(userPreferences),
|
||||
};
|
||||
}
|
||||
|
||||
requestedTasks = requestedTasks.filter(selectedFilter.filterFn);
|
||||
if (selectedFilter.sort) {
|
||||
requestedTasks = sortBy(requestedTasks, selectedFilter.sort);
|
||||
}
|
||||
|
||||
return requestedTasks;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,3 +5,15 @@ export function data (store) {
|
||||
export function gems (store) {
|
||||
return store.state.user.data.balance * 4;
|
||||
}
|
||||
|
||||
export function buffs (store) {
|
||||
return (key) => store.state.user.data.stats.buffs[key];
|
||||
}
|
||||
|
||||
export function preferences (store) {
|
||||
return store.state.user.data.preferences;
|
||||
}
|
||||
|
||||
export function tasksOrder (store) {
|
||||
return (type) => store.state.user.tasksOrder[`${type}s`];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"innCheckIn": "Rest in the Inn",
|
||||
"innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your Party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
|
||||
"innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your Party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
|
||||
"innCheckOutBanner": "You are currently checked into the Inn. Your Dailies won't damage you and you won't make progress towards Quests.",
|
||||
"resumeDamage": "Resume Damage",
|
||||
"helpfulLinks": "Helpful Links",
|
||||
"communityGuidelinesLink": "Community Guidelines",
|
||||
"lookingForGroup": "Looking for Group (Party Wanted) Posts",
|
||||
@@ -226,6 +228,7 @@
|
||||
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
||||
"partyMustbePrivate": "Parties must be private",
|
||||
"userAlreadyInGroup": "UserID: <%= userId %>, User \"<%= username %>\" already in that group.",
|
||||
"youAreAlreadyInGroup": "You are already a member of this group.",
|
||||
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
|
||||
"userAlreadyInvitedToGroup": "UserID: <%= userId %>, User \"<%= username %>\" already invited to that group.",
|
||||
"userAlreadyPendingInvitation": "UserID: <%= userId %>, User \"<%= username %>\" already pending invitation.",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"messageGroupChatAdminClearFlagCount": "Only an admin can clear the flag count!",
|
||||
"messageCannotFlagSystemMessages": "You cannot flag a system message. If you need to report a violation of the Community Guidelines related to this message, please email a screenshot and explanation to Lemoness at <%= communityManagerEmail %>.",
|
||||
"messageGroupChatSpam": "Whoops, looks like you're posting too many messages! Please wait a minute and try again. The Tavern chat only holds 200 messages at a time, so Habitica encourages posting longer, more thoughtful messages and consolidating replies. Can't wait to hear what you have to say. :)",
|
||||
"messageCannotLeaveWhileQuesting": "You cannot accept this party invitation while you are in a quest. If you'd like to join this party, you must first abort your quest, which you can do from your party screen. You will be given back the quest scroll.",
|
||||
|
||||
"messageUserOperationProtected": "path `<%= operation %>` was not saved, as it's a protected path.",
|
||||
"messageUserOperationNotFound": "<%= operation %> operation not found",
|
||||
|
||||
@@ -72,11 +72,13 @@
|
||||
"questVice1Text": "Vice, Part 1: Free Yourself of the Dragon's Influence",
|
||||
"questVice1Notes": "<p>They say there lies a terrible evil in the caverns of Mt. Habitica. A monster whose presence twists the wills of the strong heroes of the land, turning them towards bad habits and laziness! The beast is a grand dragon of immense power and comprised of the shadows themselves: Vice, the treacherous Shadow Wyrm. Brave Habiteers, stand up and defeat this foul beast once and for all, but only if you believe you can stand against its immense power. </p><h3>Vice Part 1: </h3><p>How can you expect to fight the beast if it already has control over you? Don't fall victim to laziness and vice! Work hard to fight against the dragon's dark influence and dispel his hold on you!</p>",
|
||||
"questVice1Boss": "Vice's Shade",
|
||||
"questVice1Completion": "With Vice's influence over you dispelled, you feel a surge of strength you didn't know you had return to you. Congratulations! But a more frightening foe awaits...",
|
||||
"questVice1DropVice2Quest": "Vice Part 2 (Scroll)",
|
||||
|
||||
"questVice2Text": "Vice, Part 2: Find the Lair of the Wyrm",
|
||||
"questVice2Notes": "With Vice's influence over you dispelled, you feel a surge of strength you didn't know you had return to you. Confident in yourselves and your ability to withstand the wyrm's influence, your party makes its way to Mt. Habitica. You approach the entrance to the mountain's caverns and pause. Swells of shadows, almost like fog, wisp out from the opening. It is near impossible to see anything in front of you. The light from your lanterns seem to end abruptly where the shadows begin. It is said that only magical light can pierce the dragon's infernal haze. If you can find enough light crystals, you could make your way to the dragon.",
|
||||
"questVice2Notes": "Confident in yourselves and your ability to withstand the influence of Vice the Shadow Wyrm, your Party makes its way to Mt. Habitica. You approach the entrance to the mountain's caverns and pause. Swells of shadows, almost like fog, wisp out from the opening. It is near impossible to see anything in front of you. The light from your lanterns seem to end abruptly where the shadows begin. It is said that only magical light can pierce the dragon's infernal haze. If you can find enough light crystals, you could make your way to the dragon.",
|
||||
"questVice2CollectLightCrystal": "Light Crystals",
|
||||
"questVice2Completion": "As you lift the final crystal aloft, the shadows are dispelled, and your path forward is clear. With a quickening heart, you step forward into the cavern.",
|
||||
"questVice2DropVice3Quest": "Vice Part 3 (Scroll)",
|
||||
|
||||
"questVice3Text": "Vice, Part 3: Vice Awakens",
|
||||
@@ -91,15 +93,17 @@
|
||||
"questMoonstone1Text": "Recidivate, Part 1: The Moonstone Chain",
|
||||
"questMoonstone1Notes": "A terrible affliction has struck Habiticans. Bad Habits thought long-dead are rising back up with a vengeance. Dishes lie unwashed, textbooks linger unread, and procrastination runs rampant!<br><br>You track some of your own returning Bad Habits to the Swamps of Stagnation and discover the culprit: the ghostly Necromancer, Recidivate. You rush in, weapons swinging, but they slide through her specter uselessly.<br><br>\"Don’t bother,\" she hisses with a dry rasp. \"Without a chain of moonstones, nothing can harm me – and master jeweler @aurakami scattered all the moonstones across Habitica long ago!\" Panting, you retreat... but you know what you must do.",
|
||||
"questMoonstone1CollectMoonstone": "Moonstones",
|
||||
"questMoonstone1Completion": "At last, you manage to pull the final moonstone from the swampy sludge. It’s time to go fashion your collection into a weapon that can finally defeat Recidivate!",
|
||||
"questMoonstone1DropMoonstone2Quest": "Recidivate, Part 2: Recidivate the Necromancer (Scroll)",
|
||||
|
||||
"questMoonstone2Text": "Recidivate, Part 2: Recidivate the Necromancer",
|
||||
"questMoonstone2Notes": "The brave weaponsmith @Inventrix helps you fashion the enchanted moonstones into a chain. You’re ready to confront Recidivate at last, but as you enter the Swamps of Stagnation, a terrible chill sweeps over you.<br><br>Rotting breath whispers in your ear. \"Back again? How delightful...\" You spin and lunge, and under the light of the moonstone chain, your weapon strikes solid flesh. \"You may have bound me to the world once more,\" Recidivate snarls, \"but now it is time for you to leave it!\"",
|
||||
"questMoonstone2Boss": "The Necromancer",
|
||||
"questMoonstone2Completion": "Recidivate staggers backwards under your final blow, and for a moment, your heart brightens – but then she throws back her head and lets out a horrible laugh. What’s happening?",
|
||||
"questMoonstone2DropMoonstone3Quest": "Recidivate, Part 3: Recidivate Transformed (Scroll)",
|
||||
|
||||
"questMoonstone3Text": "Recidivate, Part 3: Recidivate Transformed",
|
||||
"questMoonstone3Notes": "Recidivate crumples to the ground, and you strike at her with the moonstone chain. To your horror, Recidivate seizes the gems, eyes burning with triumph.<br><br>\"Foolish creature of flesh!\" she shouts. \"These moonstones will restore me to a physical form, true, but not as you imagined. As the full moon waxes from the dark, so too does my power flourish, and from the shadows I summon the specter of your most feared foe!\"<br><br>A sickly green fog rises from the swamp, and Recidivate’s body writhes and contorts into a shape that fills you with dread – the undead body of Vice, horribly reborn.",
|
||||
"questMoonstone3Notes": "Laughing wickedly, Recidivate crumples to the ground, and you strike at her again with the moonstone chain. To your horror, Recidivate seizes the gems, eyes burning with triumph.<br><br>\"Foolish creature of flesh!\" she shouts. \"These moonstones will restore me to a physical form, true, but not as you imagined. As the full moon waxes from the dark, so too does my power flourish, and from the shadows I summon the specter of your most feared foe!\"<br><br>A sickly green fog rises from the swamp, and Recidivate’s body writhes and contorts into a shape that fills you with dread – the undead body of Vice, horribly reborn.",
|
||||
"questMoonstone3Completion": "Your breath comes hard and sweat stings your eyes as the undead Wyrm collapses. The remains of Recidivate dissipate into a thin grey mist that clears quickly under the onslaught of a refreshing breeze, and you hear the distant, rallying cries of Habiticans defeating their Bad Habits for once and for all.<br><br>@Baconsaur the beast master swoops down on a gryphon. \"I saw the end of your battle from the sky, and I was greatly moved. Please, take this enchanted tunic – your bravery speaks of a noble heart, and I believe you were meant to have it.\"",
|
||||
"questMoonstone3Boss": "Necro-Vice",
|
||||
"questMoonstone3DropRottenMeat": "Rotten Meat (Food)",
|
||||
@@ -109,11 +113,13 @@
|
||||
"questGoldenknight1Text": "The Golden Knight, Part 1: A Stern Talking-To",
|
||||
"questGoldenknight1Notes": "The Golden Knight has been getting on poor Habiticans' cases. Didn't do all of your Dailies? Checked off a negative Habit? She will use this as a reason to harass you about how you should follow her example. She is the shining example of a perfect Habitican, and you are naught but a failure. Well, that is not nice at all! Everyone makes mistakes. They should not have to be met with such negativity for it. Perhaps it is time you gather some testimonies from hurt Habiticans and give the Golden Knight a stern talking-to!",
|
||||
"questGoldenknight1CollectTestimony": "Testimonies",
|
||||
"questGoldenknight1Completion": "Look at all these testimonies! Surely this will be enough to convince the Golden Knight. Now all you need to do is find her.",
|
||||
"questGoldenknight1DropGoldenknight2Quest": "The Golden Knight Part 2: Gold Knight (Scroll)",
|
||||
|
||||
"questGoldenknight2Text": "The Golden Knight, Part 2: Gold Knight",
|
||||
"questGoldenknight2Notes": "Armed with dozens of Habiticans' testimonies, you finally confront the Golden Knight. You begin to recite the Habitcans' complaints to her, one by one. \"And @Pfeffernusse says that your constant bragging-\" The knight raises her hand to silence you and scoffs, \"Please, these people are merely jealous of my success. Instead of complaining, they should simply work as hard as I! Perhaps I shall show you the power you can attain through diligence such as mine!\" She raises her morningstar and prepares to attack you!",
|
||||
"questGoldenknight2Boss": "Gold Knight",
|
||||
"questGoldenknight2Completion": "The Golden Knight lowers her Morningstar in consternation. “I apologize for my rash outburst,” she says. “The truth is, it’s painful to think that I’ve been inadvertently hurting others, and it made me lash out in defense… but perhaps I can still apologize?",
|
||||
"questGoldenknight2DropGoldenknight3Quest": "The Golden Knight Part 3: The Iron Knight (Scroll)",
|
||||
|
||||
"questGoldenknight3Text": "The Golden Knight, Part 3: The Iron Knight",
|
||||
@@ -160,14 +166,16 @@
|
||||
"questAtom1Notes": "You reach the shores of Washed-Up Lake for some well-earned relaxation... But the lake is polluted with unwashed dishes! How did this happen? Well, you simply cannot allow the lake to be in this state. There is only one thing you can do: clean the dishes and save your vacation spot! Better find some soap to clean up this mess. A lot of soap...",
|
||||
"questAtom1CollectSoapBars": "Bars of Soap",
|
||||
"questAtom1Drop": "The SnackLess Monster (Scroll)",
|
||||
"questAtom1Completion": "After some thorough scrubbing, all the dishes are stacked safely on the shore! You stand back and proudly survey your hard work.",
|
||||
|
||||
"questAtom2Text": "Attack of the Mundane, Part 2: The SnackLess Monster",
|
||||
"questAtom2Notes": "Phew, this place is looking a lot nicer with all these dishes cleaned. Maybe, you can finally have some fun now. Oh - there seems to be a pizza box floating in the lake. Well, what's one more thing to clean really? But alas, it is no mere pizza box! With a sudden rush the box lifts from the water to reveal itself to be the head of a monster. It cannot be! The fabled SnackLess Monster?! It is said it has existed hidden in the lake since prehistoric times: a creature spawned from the leftover food and trash of the ancient Habiticans. Yuck!",
|
||||
"questAtom2Boss": "The SnackLess Monster",
|
||||
"questAtom2Drop": "The Laundromancer (Scroll)",
|
||||
"questAtom2Completion": "With a deafening cry, and five delicious types of cheese bursting from its mouth, the Snackless Monster falls to pieces. Well done, brave adventurer! But wait... is there something else wrong with the lake?",
|
||||
|
||||
"questAtom3Text": "Attack of the Mundane, Part 3: The Laundromancer",
|
||||
"questAtom3Notes": "With a deafening cry, and five delicious types of cheese bursting from its mouth, the SnackLess Monster falls to pieces. \"HOW DARE YOU!\" booms a voice from beneath the water's surface. A robed, blue figure emerges from the water, wielding a magic toilet brush. Filthy laundry begins to bubble up to the surface of the lake. \"I am the Laundromancer!\" he angrily announces. \"You have some nerve - washing my delightfully dirty dishes, destroying my pet, and entering my domain with such clean clothes. Prepare to feel the soggy wrath of my anti-laundry magic!\"",
|
||||
"questAtom3Notes": "Just when you thought that your trials had ended, Washed-Up Lake begins to froth violently. “HOW DARE YOU!” booms a voice from beneath the water's surface. A robed, blue figure emerges from the water, wielding a magic toilet brush. Filthy laundry begins to bubble up to the surface of the lake. \"I am the Laundromancer!\" he angrily announces. \"You have some nerve - washing my delightfully dirty dishes, destroying my pet, and entering my domain with such clean clothes. Prepare to feel the soggy wrath of my anti-laundry magic!\"",
|
||||
"questAtom3Completion": "The wicked Laundromancer has been defeated! Clean laundry falls in piles all around you. Things are looking much better around here. As you begin to wade through the freshly pressed armor, a glint of metal catches your eye, and your gaze falls upon a gleaming helm. The original owner of this shining item may be unknown, but as you put it on, you feel the warming presence of a generous spirit. Too bad they didn't sew on a nametag.",
|
||||
"questAtom3Boss": "The Laundromancer",
|
||||
"questAtom3DropPotion": "Base Hatching Potion",
|
||||
|
||||
@@ -411,6 +411,7 @@ let quests = {
|
||||
vice1: {
|
||||
text: t('questVice1Text'),
|
||||
notes: t('questVice1Notes'),
|
||||
completion: t('questVice1Completion'),
|
||||
group: 'questGroupVice',
|
||||
value: 4,
|
||||
lvl: 30,
|
||||
@@ -436,6 +437,7 @@ let quests = {
|
||||
vice2: {
|
||||
text: t('questVice2Text'),
|
||||
notes: t('questVice2Notes'),
|
||||
completion: t('questVice2Completion'),
|
||||
group: 'questGroupVice',
|
||||
value: 4,
|
||||
lvl: 30,
|
||||
@@ -664,6 +666,7 @@ let quests = {
|
||||
atom1: {
|
||||
text: t('questAtom1Text'),
|
||||
notes: t('questAtom1Notes'),
|
||||
completion: t('questAtom1Completion'),
|
||||
group: 'questGroupAtom',
|
||||
value: 4,
|
||||
lvl: 15,
|
||||
@@ -690,6 +693,7 @@ let quests = {
|
||||
atom2: {
|
||||
text: t('questAtom2Text'),
|
||||
notes: t('questAtom2Notes'),
|
||||
completion: t('questAtom2Completion'),
|
||||
group: 'questGroupAtom',
|
||||
previous: 'atom1',
|
||||
value: 4,
|
||||
@@ -846,6 +850,7 @@ let quests = {
|
||||
moonstone1: {
|
||||
text: t('questMoonstone1Text'),
|
||||
notes: t('questMoonstone1Notes'),
|
||||
completion: t('questMoonstone1Completion'),
|
||||
group: 'questGroupMoonstone',
|
||||
value: 4,
|
||||
lvl: 60,
|
||||
@@ -872,6 +877,7 @@ let quests = {
|
||||
moonstone2: {
|
||||
text: t('questMoonstone2Text'),
|
||||
notes: t('questMoonstone2Notes'),
|
||||
completion: t('questMoonstone2Completion'),
|
||||
group: 'questGroupMoonstone',
|
||||
value: 4,
|
||||
lvl: 60,
|
||||
@@ -956,6 +962,7 @@ let quests = {
|
||||
goldenknight1: {
|
||||
text: t('questGoldenknight1Text'),
|
||||
notes: t('questGoldenknight1Notes'),
|
||||
completion: t('questGoldenknight1Completion'),
|
||||
group: 'questGroupGoldenknight',
|
||||
value: 4,
|
||||
lvl: 40,
|
||||
@@ -982,6 +989,7 @@ let quests = {
|
||||
goldenknight2: {
|
||||
text: t('questGoldenknight2Text'),
|
||||
notes: t('questGoldenknight2Notes'),
|
||||
completion: t('questGoldenknight2Completion'),
|
||||
group: 'questGroupGoldenknight',
|
||||
value: 4,
|
||||
previous: 'goldenknight1',
|
||||
|
||||
@@ -41,9 +41,9 @@ module.exports = function randomDrop (user, options, req = {}, analytics) {
|
||||
(1 + (user.contributor.level / 40 || 0)) * // Contrib levels: +2.5% per level
|
||||
(1 + (user.achievements.rebirths / 20 || 0)) * // Rebirths: +5% per achievement
|
||||
(1 + (user.achievements.streak / 200 || 0)) * // Streak achievements: +0.5% per achievement
|
||||
(user._tmp.crit || 1) * (1 + 0.5 * (reduce(task.checklist, (m, i) => {
|
||||
return m + (i.completed ? 1 : 0); // +50% per checklist item complete. TODO: make this into X individual drop chances instead
|
||||
}, 0) || 0));
|
||||
(user._tmp.crit || 1) * (1 + 0.5 * (reduce(task.checklist, (m, i) => { // +50% per checklist item complete. TODO: make this into X individual drop chances instead
|
||||
return m + (i.completed ? 1 : 0); // eslint-disable-line indent
|
||||
}, 0) || 0)); // eslint-disable-line indent
|
||||
chance = diminishingReturns(chance, 0.75);
|
||||
|
||||
if (predictableRandom() < chance) {
|
||||
|
||||
@@ -40,3 +40,12 @@ export class NotFound extends CustomError {
|
||||
this.message = customMessage || 'Not found.';
|
||||
}
|
||||
}
|
||||
|
||||
export class NotImplementedError extends CustomError {
|
||||
constructor (str) {
|
||||
super();
|
||||
this.name = this.constructor.name;
|
||||
|
||||
this.message = `Method: '${str}' not implemented`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ module.exports = {
|
||||
],
|
||||
|
||||
availableQuests: [
|
||||
'egg',
|
||||
],
|
||||
|
||||
featuredSet: 'comfortingKittySet',
|
||||
|
||||
@@ -478,7 +478,7 @@ shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, lang
|
||||
};
|
||||
|
||||
category.items = map(quests, (quest) => {
|
||||
return getItemInfo(user, 'seasonalQuest', quest, language);
|
||||
return getItemInfo(user, 'seasonalQuest', quest, officialPinnedItems, language);
|
||||
});
|
||||
|
||||
categories.push(category);
|
||||
|
||||
127
website/common/script/ops/buy/abstractBuyOperation.js
Normal file
127
website/common/script/ops/buy/abstractBuyOperation.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import i18n from '../../i18n';
|
||||
import {
|
||||
NotAuthorized, NotImplementedError,
|
||||
} from '../../libs/errors';
|
||||
import _merge from 'lodash/merge';
|
||||
import _get from 'lodash/get';
|
||||
|
||||
export class AbstractBuyOperation {
|
||||
/**
|
||||
* @param {User} user - the User-Object
|
||||
* @param {Request} req - the Request-Object
|
||||
* @param {analytics} analytics
|
||||
*/
|
||||
constructor (user, req, analytics) {
|
||||
this.user = user;
|
||||
this.req = req || {};
|
||||
this.analytics = analytics;
|
||||
|
||||
this.quantity = _get(req, 'quantity', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to get the translated string without passing `req.language`
|
||||
* @param {String} key - translation key
|
||||
* @param {*=} params
|
||||
* @returns {*|string}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
i18n (key, params = {}) {
|
||||
return i18n.t.apply(null, [...arguments, this.req.language]);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the Operation allows purchasing items by quantity
|
||||
* @returns Boolean
|
||||
*/
|
||||
multiplePurchaseAllowed () {
|
||||
throw new NotImplementedError('multiplePurchaseAllowed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is called to save the params as class-fields in order to access them
|
||||
*/
|
||||
extractAndValidateParams () {
|
||||
throw new NotImplementedError('extractAndValidateParams');
|
||||
}
|
||||
|
||||
executeChanges () {
|
||||
throw new NotImplementedError('executeChanges');
|
||||
}
|
||||
|
||||
analyticsData () {
|
||||
throw new NotImplementedError('sendToAnalytics');
|
||||
}
|
||||
|
||||
purchase () {
|
||||
if (!this.multiplePurchaseAllowed() && this.quantity > 1) {
|
||||
throw new NotAuthorized(this.i18n('messageNotAbleToBuyInBulk'));
|
||||
}
|
||||
|
||||
this.extractAndValidateParams(this.user, this.req);
|
||||
|
||||
let resultObj = this.executeChanges(this.user, this.item, this.req);
|
||||
|
||||
if (this.analytics) {
|
||||
this.sendToAnalytics(this.analyticsData());
|
||||
}
|
||||
|
||||
return resultObj;
|
||||
}
|
||||
|
||||
sendToAnalytics (additionalData = {}) {
|
||||
// spread-operator produces an "unexpected token" error
|
||||
let analyticsData = _merge(additionalData, {
|
||||
// ...additionalData,
|
||||
uuid: this.user._id,
|
||||
category: 'behavior',
|
||||
headers: this.req.headers,
|
||||
});
|
||||
|
||||
if (this.multiplePurchaseAllowed()) {
|
||||
analyticsData.quantityPurchased = this.quantity;
|
||||
}
|
||||
|
||||
this.analytics.track('acquire item', analyticsData);
|
||||
}
|
||||
}
|
||||
|
||||
export class AbstractGoldItemOperation extends AbstractBuyOperation {
|
||||
constructor (user, req, analytics) {
|
||||
super(user, req, analytics);
|
||||
}
|
||||
|
||||
getItemValue (item) {
|
||||
return item.value;
|
||||
}
|
||||
|
||||
canUserPurchase (user, item) {
|
||||
this.item = item;
|
||||
let itemValue = this.getItemValue(item);
|
||||
|
||||
let userGold = user.stats.gp;
|
||||
|
||||
if (userGold < itemValue * this.quantity) {
|
||||
throw new NotAuthorized(this.i18n('messageNotEnoughGold'));
|
||||
}
|
||||
|
||||
if (item.canOwn && !item.canOwn(user)) {
|
||||
throw new NotAuthorized(this.i18n('cannotBuyItem'));
|
||||
}
|
||||
}
|
||||
|
||||
substractCurrency (user, item, quantity = 1) {
|
||||
let itemValue = this.getItemValue(item);
|
||||
|
||||
user.stats.gp -= itemValue * quantity;
|
||||
}
|
||||
|
||||
analyticsData () {
|
||||
return {
|
||||
itemKey: this.item.key,
|
||||
itemType: 'Market',
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: this.getItemValue(this.item),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '../../libs/errors';
|
||||
import buyHealthPotion from './buyHealthPotion';
|
||||
import buyArmoire from './buyArmoire';
|
||||
import buyGear from './buyGear';
|
||||
import {BuyMarketGearOperation} from './buyMarketGear';
|
||||
import buyMysterySet from './buyMysterySet';
|
||||
import buyQuest from './buyQuest';
|
||||
import buySpecialSpell from './buySpecialSpell';
|
||||
@@ -58,10 +58,13 @@ module.exports = function buy (user, req = {}, analytics) {
|
||||
case 'special':
|
||||
buyRes = buySpecialSpell(user, req, analytics);
|
||||
break;
|
||||
default:
|
||||
buyRes = buyGear(user, req, analytics);
|
||||
default: {
|
||||
const buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||
|
||||
buyRes = buyOp.purchase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return buyRes;
|
||||
};
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import content from '../../content/index';
|
||||
import i18n from '../../i18n';
|
||||
import get from 'lodash/get';
|
||||
import pick from 'lodash/pick';
|
||||
import splitWhitespace from '../../libs/splitWhitespace';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import handleTwoHanded from '../../fns/handleTwoHanded';
|
||||
import ultimateGear from '../../fns/ultimateGear';
|
||||
|
||||
import { removePinnedGearAddPossibleNewOnes } from '../pinnedGearUtils';
|
||||
|
||||
module.exports = function buyGear (user, req = {}, analytics) {
|
||||
let key = get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
|
||||
let item = content.gear.flat[key];
|
||||
|
||||
if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
|
||||
|
||||
if (user.stats.gp < item.value) {
|
||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
}
|
||||
|
||||
if (item.canOwn && !item.canOwn(user)) {
|
||||
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
|
||||
}
|
||||
|
||||
let message;
|
||||
|
||||
if (user.items.gear.owned[item.key]) {
|
||||
throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language));
|
||||
}
|
||||
|
||||
let itemIndex = Number(item.index);
|
||||
|
||||
if (Number.isInteger(itemIndex) && content.classes.includes(item.klass)) {
|
||||
let previousLevelGear = key.replace(/[0-9]/, itemIndex - 1);
|
||||
let hasPreviousLevelGear = user.items.gear.owned[previousLevelGear];
|
||||
let checkIndexToType = itemIndex > (item.type === 'weapon' || item.type === 'shield' && item.klass === 'rogue' ? 0 : 1);
|
||||
|
||||
if (checkIndexToType && !hasPreviousLevelGear) {
|
||||
throw new NotAuthorized(i18n.t('previousGearNotOwned', req.language));
|
||||
}
|
||||
}
|
||||
|
||||
if (user.preferences.autoEquip) {
|
||||
user.items.gear.equipped[item.type] = item.key;
|
||||
message = handleTwoHanded(user, item, undefined, req);
|
||||
}
|
||||
|
||||
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
|
||||
|
||||
if (item.last) ultimateGear(user);
|
||||
|
||||
user.stats.gp -= item.value;
|
||||
|
||||
if (!message) {
|
||||
message = i18n.t('messageBought', {
|
||||
itemText: item.text(req.language),
|
||||
}, req.language);
|
||||
}
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('acquire item', {
|
||||
uuid: user._id,
|
||||
itemKey: key,
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: item.value,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('items achievements stats flags pinnedItems')),
|
||||
message,
|
||||
];
|
||||
};
|
||||
78
website/common/script/ops/buy/buyMarketGear.js
Normal file
78
website/common/script/ops/buy/buyMarketGear.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import content from '../../content/index';
|
||||
import get from 'lodash/get';
|
||||
import pick from 'lodash/pick';
|
||||
import splitWhitespace from '../../libs/splitWhitespace';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import handleTwoHanded from '../../fns/handleTwoHanded';
|
||||
import ultimateGear from '../../fns/ultimateGear';
|
||||
|
||||
import {removePinnedGearAddPossibleNewOnes} from '../pinnedGearUtils';
|
||||
|
||||
import { AbstractGoldItemOperation } from './abstractBuyOperation';
|
||||
|
||||
export class BuyMarketGearOperation extends AbstractGoldItemOperation {
|
||||
constructor (user, req, analytics) {
|
||||
super(user, req, analytics);
|
||||
}
|
||||
|
||||
multiplePurchaseAllowed () {
|
||||
return false;
|
||||
}
|
||||
|
||||
extractAndValidateParams (user, req) {
|
||||
let key = this.key = get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(this.i18n('missingKeyParam'));
|
||||
|
||||
let item = content.gear.flat[key];
|
||||
|
||||
if (!item) throw new NotFound(this.i18n('itemNotFound', {key}));
|
||||
|
||||
this.canUserPurchase(user, item);
|
||||
|
||||
if (user.items.gear.owned[item.key]) {
|
||||
throw new NotAuthorized(this.i18n('equipmentAlreadyOwned'));
|
||||
}
|
||||
|
||||
let itemIndex = Number(item.index);
|
||||
|
||||
if (Number.isInteger(itemIndex) && content.classes.includes(item.klass)) {
|
||||
let previousLevelGear = key.replace(/[0-9]/, itemIndex - 1);
|
||||
let hasPreviousLevelGear = user.items.gear.owned[previousLevelGear];
|
||||
let checkIndexToType = itemIndex > (item.type === 'weapon' || item.type === 'shield' && item.klass === 'rogue' ? 0 : 1);
|
||||
|
||||
if (checkIndexToType && !hasPreviousLevelGear) {
|
||||
throw new NotAuthorized(this.i18n('previousGearNotOwned'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executeChanges (user, item, req) {
|
||||
let message;
|
||||
|
||||
if (user.preferences.autoEquip) {
|
||||
user.items.gear.equipped[item.type] = item.key;
|
||||
message = handleTwoHanded(user, item, undefined, req);
|
||||
}
|
||||
|
||||
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
|
||||
|
||||
if (item.last) ultimateGear(user);
|
||||
|
||||
this.substractCurrency(user, item);
|
||||
|
||||
if (!message) {
|
||||
message = this.i18n('messageBought', {
|
||||
itemText: item.text(req.language),
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('items achievements stats flags pinnedItems')),
|
||||
message,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import splitWhitespace from '../libs/splitWhitespace';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
module.exports = function releaseBoth (user, req = {}, analytics) {
|
||||
module.exports = function releaseBoth (user, req = {}) {
|
||||
let animal;
|
||||
|
||||
if (!user.achievements.triadBingo) {
|
||||
@@ -22,19 +22,20 @@ module.exports = function releaseBoth (user, req = {}, analytics) {
|
||||
let giveBeastMasterAchievement = true;
|
||||
let giveMountMasterAchievement = true;
|
||||
|
||||
if (!user.achievements.triadBingo) {
|
||||
if (analytics) {
|
||||
analytics.track('release pets & mounts', {
|
||||
uuid: user._id,
|
||||
acquireMethod: 'Gems',
|
||||
gemCost: 6,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
user.balance -= 1.5;
|
||||
}
|
||||
// @TODO: We are only offering the free version now
|
||||
// if (!user.achievements.triadBingo) {
|
||||
// if (analytics) {
|
||||
// analytics.track('release pets & mounts', {
|
||||
// uuid: user._id,
|
||||
// acquireMethod: 'Gems',
|
||||
// gemCost: 6,
|
||||
// category: 'behavior',
|
||||
// headers: req.headers,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// user.balance -= 1.5;
|
||||
// }
|
||||
|
||||
let mountInfo = content.mountInfo[user.items.currentMount];
|
||||
|
||||
|
||||
@@ -629,7 +629,7 @@ api.updateEmail = {
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let emailAlreadyInUse = await User.findOne({
|
||||
'auth.local.email': req.body.newEmail,
|
||||
'auth.local.email': req.body.newEmail.toLowerCase(),
|
||||
}).select({_id: 1}).lean().exec();
|
||||
|
||||
if (emailAlreadyInUse) throw new NotAuthorized(res.t('cannotFulfillReq', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL }));
|
||||
@@ -643,7 +643,7 @@ api.updateEmail = {
|
||||
await passwordUtils.convertToBcrypt(user, password);
|
||||
}
|
||||
|
||||
user.auth.local.email = req.body.newEmail;
|
||||
user.auth.local.email = req.body.newEmail.toLowerCase();
|
||||
await user.save();
|
||||
|
||||
return res.respond(200, { email: user.auth.local.email });
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import * as Tasks from '../../models/task';
|
||||
import Bluebird from 'bluebird';
|
||||
import csvStringify from '../../libs/csvStringify';
|
||||
import {
|
||||
createTasks,
|
||||
@@ -254,7 +253,7 @@ api.joinChallenge = {
|
||||
addUserJoinChallengeNotification(user);
|
||||
|
||||
// Add all challenge's tasks to user's tasks and save the challenge
|
||||
let results = await Bluebird.all([challenge.syncToUser(user), challenge.save()]);
|
||||
let results = await Promise.all([challenge.syncToUser(user), challenge.save()]);
|
||||
|
||||
let response = results[1].toJSON();
|
||||
response.group = getChallengeGroupResponse(group);
|
||||
@@ -306,7 +305,7 @@ api.leaveChallenge = {
|
||||
if (!challenge.isMember(user)) throw new NotAuthorized(res.t('challengeMemberNotFound'));
|
||||
|
||||
// Unlink challenge's tasks from user's tasks and save the challenge
|
||||
await Bluebird.all([challenge.unlinkTasks(user, keep), challenge.save()]);
|
||||
await Promise.all([challenge.unlinkTasks(user, keep), challenge.save()]);
|
||||
|
||||
res.analytics.track('challenge leave', {
|
||||
uuid: user._id,
|
||||
@@ -356,16 +355,21 @@ api.getUserChallenges = {
|
||||
let challenges = await Challenge.find({
|
||||
$or: orOptions,
|
||||
})
|
||||
.sort('-official -createdAt')
|
||||
.sort('-createdAt')
|
||||
// see below why we're not using populate
|
||||
// .populate('group', basicGroupFields)
|
||||
// .populate('leader', nameFields)
|
||||
.exec();
|
||||
|
||||
let resChals = challenges.map(challenge => challenge.toJSON());
|
||||
|
||||
resChals = _.orderBy(resChals, [challenge => {
|
||||
return challenge.categories.map(category => category.slug).includes('habitica_official');
|
||||
}], ['desc']);
|
||||
|
||||
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||
await Bluebird.all(resChals.map((chal, index) => {
|
||||
return Bluebird.all([
|
||||
await Promise.all(resChals.map((chal, index) => {
|
||||
return Promise.all([
|
||||
User.findById(chal.leader).select(nameFields).exec(),
|
||||
Group.findById(chal.group).select(basicGroupFields).exec(),
|
||||
]).then(populatedData => {
|
||||
@@ -413,13 +417,18 @@ api.getGroupChallenges = {
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let challenges = await Challenge.find({group: groupId})
|
||||
.sort('-official -createdAt')
|
||||
.sort('-createdAt')
|
||||
// .populate('leader', nameFields) // Only populate the leader as the group is implicit
|
||||
.exec();
|
||||
|
||||
let resChals = challenges.map(challenge => challenge.toJSON());
|
||||
|
||||
resChals = _.orderBy(resChals, [challenge => {
|
||||
return challenge.categories.map(category => category.slug).includes('habitica_official');
|
||||
}], ['desc']);
|
||||
|
||||
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||
await Bluebird.all(resChals.map((chal, index) => {
|
||||
await Promise.all(resChals.map((chal, index) => {
|
||||
return User
|
||||
.findById(chal.leader)
|
||||
.select(nameFields)
|
||||
@@ -511,7 +520,7 @@ api.exportChallengeCsv = {
|
||||
// In v2 this used the aggregation framework to run some computation on MongoDB but then iterated through all
|
||||
// results on the server so the perf difference isn't that big (hopefully)
|
||||
|
||||
let [members, tasks] = await Bluebird.all([
|
||||
let [members, tasks] = await Promise.all([
|
||||
User.find({challenges: challengeId})
|
||||
.select(nameFields)
|
||||
.sort({_id: 1})
|
||||
|
||||
@@ -14,7 +14,6 @@ import pusher from '../../libs/pusher';
|
||||
import { getAuthorEmailFromMessage } from '../../libs/chat';
|
||||
import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory';
|
||||
import nconf from 'nconf';
|
||||
import Bluebird from 'bluebird';
|
||||
import bannedWords from '../../libs/bannedWords';
|
||||
import guildsAllowingBannedWords from '../../libs/guildsAllowingBannedWords';
|
||||
import { getMatchesByWordArray } from '../../libs/stringUtils';
|
||||
@@ -184,7 +183,7 @@ api.postChat = {
|
||||
toSave.push(user.save());
|
||||
}
|
||||
|
||||
let [savedGroup] = await Bluebird.all(toSave);
|
||||
let [savedGroup] = await Promise.all(toSave);
|
||||
|
||||
// realtime chat is only enabled for private groups (for now only for parties)
|
||||
if (savedGroup.privacy === 'private' && savedGroup.type === 'party') {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import common from '../../../common';
|
||||
import _ from 'lodash';
|
||||
import { langCodes } from '../../libs/i18n';
|
||||
import Bluebird from 'bluebird';
|
||||
import fsCallback from 'fs';
|
||||
import path from 'path';
|
||||
import logger from '../../libs/logger';
|
||||
import util from 'util';
|
||||
|
||||
// Transform fs methods that accept callbacks in ones that return promises
|
||||
const fs = {
|
||||
readFile: Bluebird.promisify(fsCallback.readFile, {context: fsCallback}),
|
||||
writeFile: Bluebird.promisify(fsCallback.writeFile, {context: fsCallback}),
|
||||
stat: Bluebird.promisify(fsCallback.stat, {context: fsCallback}),
|
||||
mkdir: Bluebird.promisify(fsCallback.mkdir, {context: fsCallback}),
|
||||
readFile: util.promisify(fsCallback.readFile).bind(fsCallback),
|
||||
writeFile: util.promisify(fsCallback.writeFile).bind(fsCallback),
|
||||
stat: util.promisify(fsCallback.stat).bind(fsCallback),
|
||||
mkdir: util.promisify(fsCallback.mkdir).bind(fsCallback),
|
||||
};
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import Bluebird from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
@@ -137,7 +136,7 @@ api.createGroup = {
|
||||
user.party._id = group._id;
|
||||
}
|
||||
|
||||
let results = await Bluebird.all([user.save(), group.save()]);
|
||||
let results = await Promise.all([user.save(), group.save()]);
|
||||
let savedGroup = results[1];
|
||||
|
||||
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||
@@ -194,7 +193,7 @@ api.createGroupPlan = {
|
||||
group.leader = user._id;
|
||||
user.guilds.push(group._id);
|
||||
|
||||
let results = await Bluebird.all([user.save(), group.save()]);
|
||||
let results = await Promise.all([user.save(), group.save()]);
|
||||
let savedGroup = results[1];
|
||||
|
||||
// Analytics
|
||||
@@ -337,7 +336,7 @@ api.getGroups = {
|
||||
|
||||
if (req.query.search) {
|
||||
filters.$or = [];
|
||||
const searchWords = req.query.search.split(' ').join('|');
|
||||
const searchWords = _.escapeRegExp(req.query.search).split(' ').join('|');
|
||||
const searchQuery = { $regex: new RegExp(`${searchWords}`, 'i') };
|
||||
filters.$or.push({name: searchQuery});
|
||||
filters.$or.push({description: searchQuery});
|
||||
@@ -519,6 +518,18 @@ api.joinGroup = {
|
||||
if (inviterParty) {
|
||||
inviter = inviterParty.inviter;
|
||||
|
||||
// If user was in a different party (when partying solo you can be invited to a new party)
|
||||
// make them leave that party before doing anything
|
||||
if (user.party._id) {
|
||||
let userPreviousParty = await Group.getGroup({user, groupId: user.party._id});
|
||||
|
||||
if (userPreviousParty.memberCount === 1 && user.party.quest.key) {
|
||||
throw new NotAuthorized(res.t('messageCannotLeaveWhileQuesting'));
|
||||
}
|
||||
|
||||
if (userPreviousParty) await userPreviousParty.leave(user);
|
||||
}
|
||||
|
||||
// Clear all invitations of new user
|
||||
user.invitations.parties = [];
|
||||
user.invitations.party = {};
|
||||
@@ -531,13 +542,6 @@ api.joinGroup = {
|
||||
group.markModified('quest.members');
|
||||
}
|
||||
|
||||
// If user was in a different party (when partying solo you can be invited to a new party)
|
||||
// make them leave that party before doing anything
|
||||
if (user.party._id) {
|
||||
let userPreviousParty = await Group.getGroup({user, groupId: user.party._id});
|
||||
if (userPreviousParty) await userPreviousParty.leave(user);
|
||||
}
|
||||
|
||||
user.party._id = group._id; // Set group as user's party
|
||||
|
||||
isUserInvited = true;
|
||||
@@ -555,7 +559,7 @@ api.joinGroup = {
|
||||
|
||||
if (isUserInvited && group.type === 'guild') {
|
||||
if (user.guilds.indexOf(group._id) !== -1) { // if user is already a member (party is checked previously)
|
||||
throw new NotAuthorized(res.t('userAlreadyInGroup'));
|
||||
throw new NotAuthorized(res.t('youAreAlreadyInGroup'));
|
||||
}
|
||||
user.guilds.push(group._id); // Add group to user's guilds
|
||||
if (!user.achievements.joinedGuild) {
|
||||
@@ -617,7 +621,7 @@ api.joinGroup = {
|
||||
}
|
||||
}
|
||||
|
||||
promises = await Bluebird.all(promises);
|
||||
promises = await Promise.all(promises);
|
||||
|
||||
let response = Group.toJSONCleanChat(promises[0], user);
|
||||
let leader = await User.findById(response.leader).select(nameFields).exec();
|
||||
@@ -915,7 +919,7 @@ api.removeGroupMember = {
|
||||
let message = req.query.message || req.body.message;
|
||||
_sendMessageToRemoved(group, member, message, isInGroup);
|
||||
|
||||
await Bluebird.all([
|
||||
await Promise.all([
|
||||
member.save(),
|
||||
group.save(),
|
||||
]);
|
||||
@@ -1167,7 +1171,7 @@ api.inviteToGroup = {
|
||||
|
||||
if (uuids) {
|
||||
let uuidInvites = uuids.map((uuid) => _inviteByUUID(uuid, group, user, req, res));
|
||||
let uuidResults = await Bluebird.all(uuidInvites);
|
||||
let uuidResults = await Promise.all(uuidInvites);
|
||||
results.push(...uuidResults);
|
||||
}
|
||||
|
||||
@@ -1175,7 +1179,7 @@ api.inviteToGroup = {
|
||||
let emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res));
|
||||
user.invitesSent += emails.length;
|
||||
await user.save();
|
||||
let emailResults = await Bluebird.all(emailInvites);
|
||||
let emailResults = await Promise.all(emailInvites);
|
||||
results.push(...emailResults);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
getUserInfo,
|
||||
sendTxn as sendTxnEmail,
|
||||
} from '../../libs/email';
|
||||
import Bluebird from 'bluebird';
|
||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
||||
import { achievements } from '../../../../website/common/';
|
||||
|
||||
@@ -552,7 +551,7 @@ api.transferGems = {
|
||||
receiver.balance += amount;
|
||||
sender.balance -= amount;
|
||||
let promises = [receiver.save(), sender.save()];
|
||||
await Bluebird.all(promises);
|
||||
await Promise.all(promises);
|
||||
|
||||
// generate the message in both languages, so both users can understand it
|
||||
let receiverLang = receiver.preferences.language;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import Bluebird from 'bluebird';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import analytics from '../../libs/analyticsService';
|
||||
import {
|
||||
@@ -109,7 +108,7 @@ api.inviteToQuest = {
|
||||
await group.startQuest(user);
|
||||
}
|
||||
|
||||
let [savedGroup] = await Bluebird.all([
|
||||
let [savedGroup] = await Promise.all([
|
||||
group.save(),
|
||||
user.save(),
|
||||
]);
|
||||
@@ -312,7 +311,7 @@ api.forceStart = {
|
||||
|
||||
await group.startQuest(user);
|
||||
|
||||
let [savedGroup] = await Bluebird.all([
|
||||
let [savedGroup] = await Promise.all([
|
||||
group.save(),
|
||||
user.save(),
|
||||
]);
|
||||
@@ -372,7 +371,7 @@ api.cancelQuest = {
|
||||
group.quest = Group.cleanGroupQuest();
|
||||
group.markModified('quest');
|
||||
|
||||
let [savedGroup] = await Bluebird.all([
|
||||
let [savedGroup] = await Promise.all([
|
||||
group.save(),
|
||||
User.update(
|
||||
{'party._id': groupId},
|
||||
@@ -441,7 +440,7 @@ api.abortQuest = {
|
||||
group.quest = Group.cleanGroupQuest();
|
||||
group.markModified('quest');
|
||||
|
||||
let [groupSaved] = await Bluebird.all([group.save(), memberUpdates, questLeaderUpdate]);
|
||||
let [groupSaved] = await Promise.all([group.save(), memberUpdates, questLeaderUpdate]);
|
||||
|
||||
res.respond(200, groupSaved.quest);
|
||||
},
|
||||
@@ -486,7 +485,7 @@ api.leaveQuest = {
|
||||
user.party.quest = Group.cleanQuestProgress();
|
||||
user.markModified('party.quest');
|
||||
|
||||
let [savedGroup] = await Bluebird.all([
|
||||
let [savedGroup] = await Promise.all([
|
||||
group.save(),
|
||||
user.save(),
|
||||
]);
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
setNextDue,
|
||||
} from '../../libs/taskManager';
|
||||
import common from '../../../common';
|
||||
import Bluebird from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import logger from '../../libs/logger';
|
||||
import moment from 'moment';
|
||||
@@ -598,7 +597,7 @@ api.scoreTask = {
|
||||
});
|
||||
|
||||
managerPromises.push(task.save());
|
||||
await Bluebird.all(managerPromises);
|
||||
await Promise.all(managerPromises);
|
||||
|
||||
throw new NotAuthorized(res.t('taskApprovalHasBeenRequested'));
|
||||
}
|
||||
@@ -647,7 +646,7 @@ api.scoreTask = {
|
||||
task.save(),
|
||||
];
|
||||
if (taskOrderPromise) promises.push(taskOrderPromise);
|
||||
let results = await Bluebird.all(promises);
|
||||
let results = await Promise.all(promises);
|
||||
|
||||
let savedUser = results[0];
|
||||
|
||||
@@ -1132,7 +1131,7 @@ api.unlinkAllTasks = {
|
||||
if (!validTasks) throw new BadRequest(res.t('cantOnlyUnlinkChalTask'));
|
||||
|
||||
if (keep === 'keep-all') {
|
||||
await Bluebird.all(tasks.map(task => {
|
||||
await Promise.all(tasks.map(task => {
|
||||
task.challenge = {};
|
||||
return task.save();
|
||||
}));
|
||||
@@ -1149,7 +1148,7 @@ api.unlinkAllTasks = {
|
||||
|
||||
toSave.push(user.save());
|
||||
|
||||
await Bluebird.all(toSave);
|
||||
await Promise.all(toSave);
|
||||
}
|
||||
|
||||
res.respond(200, {});
|
||||
@@ -1199,7 +1198,7 @@ api.unlinkOneTask = {
|
||||
} else { // remove
|
||||
if (task.type !== 'todo' || !task.completed) { // eslint-disable-line no-lonely-if
|
||||
removeFromArray(user.tasksOrder[`${task.type}s`], taskId);
|
||||
await Bluebird.all([user.save(), task.remove()]);
|
||||
await Promise.all([user.save(), task.remove()]);
|
||||
} else {
|
||||
await task.remove();
|
||||
}
|
||||
@@ -1317,7 +1316,7 @@ api.deleteTask = {
|
||||
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
|
||||
if (!challenge) user._v++;
|
||||
|
||||
await Bluebird.all([taskOrderUpdate, task.remove()]);
|
||||
await Promise.all([taskOrderUpdate, task.remove()]);
|
||||
} else {
|
||||
await task.remove();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import Bluebird from 'bluebird';
|
||||
import * as Tasks from '../../../models/task';
|
||||
import { model as Group } from '../../../models/group';
|
||||
import { model as User } from '../../../models/user';
|
||||
@@ -205,7 +204,7 @@ api.assignTask = {
|
||||
let promises = [];
|
||||
promises.push(group.syncTask(task, assignedUser));
|
||||
promises.push(group.save());
|
||||
await Bluebird.all(promises);
|
||||
await Promise.all(promises);
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
@@ -350,7 +349,7 @@ api.approveTask = {
|
||||
|
||||
managerPromises.push(task.save());
|
||||
managerPromises.push(assignedUser.save());
|
||||
await Bluebird.all(managerPromises);
|
||||
await Promise.all(managerPromises);
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user