Compare commits
322 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5575b3593 | |||
| ace964f2b3 | |||
| 12b045093a | |||
| 8cd9536bdc | |||
| 08f0374b46 | |||
| 3582e233be | |||
| 3974adcb65 | |||
| 57d3fea523 | |||
| 173d7a178c | |||
| 7db093d2bb | |||
| 06d2ffb37d | |||
| 40997854dd | |||
| 4c4d0be31f | |||
| b8cf1b895f | |||
| a08ecbe044 | |||
| f3771f4869 | |||
| e0eed8238e | |||
| 6baf08d461 | |||
| 535fddf92d | |||
| ef97f301d9 | |||
| eea79ce1b6 | |||
| 191fee524c | |||
| 098f53bfa9 | |||
| e9ee2d3fdd | |||
| 4c988691cf | |||
| 3c8be16135 | |||
| 9890e0079a | |||
| 1530ab44e9 | |||
| 586897fbfc | |||
| f75a6eb11d | |||
| 155d6d5af6 | |||
| 004f1ee2dc | |||
| c61bdaf563 | |||
| 85e14bb100 | |||
| 8e9b469d8d | |||
| 6e5cac88fc | |||
| 80acb70718 | |||
| 24430861ce | |||
| e60285e7d9 | |||
| f030135c82 | |||
| 2c29310466 | |||
| 13eef6e4cf | |||
| 13c0d12045 | |||
| 6456984f57 | |||
| cfc1a12930 | |||
| 9ba4687478 | |||
| 6d987a9579 | |||
| 4702479156 | |||
| b384cd4eb8 | |||
| d2bd7dc325 | |||
| 719fab8d4b | |||
| 24841346dc | |||
| 4c34c68d78 | |||
| 9a8d1854b9 | |||
| 10f5011781 | |||
| 9a896470d5 | |||
| 6b0b393e32 | |||
| 014a4b653a | |||
| 37e5d6b40a | |||
| fb780c9a2d | |||
| 5f440f1bfa | |||
| d4f9555f11 | |||
| 203d97423a | |||
| 0f4711c358 | |||
| 1a86943711 | |||
| 21185b689c | |||
| 5a85e0730c | |||
| 771558e1fd | |||
| e6f903fd2e | |||
| 5c13bf1980 | |||
| 36a4ec69d5 | |||
| c6ba1d8402 | |||
| 0081bad831 | |||
| abdb6244d3 | |||
| 90f1977a49 | |||
| bc33e4349d | |||
| 61f3d8d61c | |||
| 38bf0b3721 | |||
| 88c8b545f4 | |||
| 184ee7262e | |||
| 6df4ce251c | |||
| 796d752974 | |||
| 008314676d | |||
| e383614107 | |||
| 2b21410abd | |||
| f364b3c06f | |||
| ae23ac12ff | |||
| e2bb7fda60 | |||
| 09d6dae75c | |||
| bc5813fd10 | |||
| 4464464c51 | |||
| 56f956be5a | |||
| 2b44d32b1c | |||
| 11347e5679 | |||
| a04479e689 | |||
| 4ce4e55e80 | |||
| 755f51b674 | |||
| 6c1b21117f | |||
| 14441701c9 | |||
| a3e6aff330 | |||
| a523d0b894 | |||
| 8a809c3828 | |||
| ccb821fd6f | |||
| 3664a1ebb1 | |||
| 509cb00374 | |||
| bc4770577a | |||
| f158852be5 | |||
| 40f433b099 | |||
| 9a1266677a | |||
| ee0f6fd78f | |||
| 3284611bbf | |||
| 986d38af69 | |||
| 7129639bbf | |||
| 6aabf7b19a | |||
| a5d9448af1 | |||
| 6e19a0ef2e | |||
| bc8b1884b7 | |||
| 1aae9638ec | |||
| e6b0c1e488 | |||
| d5bbc9599c | |||
| a2f191089c | |||
| 75c8486b1a | |||
| 20854057ad | |||
| ae3f064197 | |||
| 67ee0b72d3 | |||
| aebf13810f | |||
| 971891dd6b | |||
| 395b8db932 | |||
| da5c3f9602 | |||
| 4c85b933cb | |||
| 82abdaa0c4 | |||
| 02c50b6126 | |||
| 3ab88bbb3f | |||
| 5251598369 | |||
| 149da578fd | |||
| 35d963a397 | |||
| cccd8c3b1b | |||
| 631d7111a5 | |||
| 89c07529ea | |||
| 595c131398 | |||
| f063b9e81c | |||
| 49a20218a5 | |||
| 95714599f0 | |||
| 5893312d75 | |||
| d70dd2e6dd | |||
| 71fa4d6cb7 | |||
| 922b2e985a | |||
| b82239811c | |||
| f0b5637e9e | |||
| f078d19e4b | |||
| 2c93b3e2e3 | |||
| 72a9417de9 | |||
| e08d0f4016 | |||
| 0435e3537a | |||
| 0aadee550e | |||
| f487837b4b | |||
| b593db2150 | |||
| a07c2e6268 | |||
| e3c552dd54 | |||
| 8d1f7e77ed | |||
| 1701fc702b | |||
| 16dc6a1b4c | |||
| d3a91aab72 | |||
| 0528ee1761 | |||
| cfd601e7bf | |||
| 1b4d670b0a | |||
| 5d81c63897 | |||
| 3654e01fee | |||
| fdbeda19e2 | |||
| db723d79a4 | |||
| d5d1bfbd99 | |||
| d06f4f4e1e | |||
| 82c4260fca | |||
| 0f3a26a490 | |||
| 9c2963e557 | |||
| cff6c5674f | |||
| d5c4e1666e | |||
| 79071e3445 | |||
| 2ea707c27c | |||
| f7b727dc95 | |||
| edcb3f4289 | |||
| bd28a282df | |||
| 9b9503b141 | |||
| 999071a15c | |||
| a78aea5456 | |||
| 6e8e7318f3 | |||
| 1c3d4a6fd5 | |||
| 9bee9d0a06 | |||
| a418752041 | |||
| f5b632e3e5 | |||
| c9b3c48379 | |||
| 9ed2359c77 | |||
| 0dc21fa868 | |||
| e2c6fb1ea2 | |||
| 7f8e44ff49 | |||
| 8ecd152b41 | |||
| e0e9381584 | |||
| ba22c18cd9 | |||
| ef3767f80b | |||
| 18db432f7f | |||
| 30d3892fb4 | |||
| 8070486def | |||
| e06a0e5e7f | |||
| 153561dd42 | |||
| 7b067de4b9 | |||
| 9453b1269e | |||
| cf536a82f8 | |||
| 5967e4356c | |||
| 6ebfa976fe | |||
| ddd5f20609 | |||
| a3f61306d3 | |||
| 712b85ce84 | |||
| 9142588ba7 | |||
| b657172a2b | |||
| b790b87ca8 | |||
| 9d3059fc30 | |||
| 647371accc | |||
| 8b084e627e | |||
| 4ac1a3e717 | |||
| a0177fa44d | |||
| 9fec111c4d | |||
| a559c1add8 | |||
| 5868849034 | |||
| c98c7ab26c | |||
| 1ef7924ba5 | |||
| 7651e6a540 | |||
| bb20c44fde | |||
| eb99ca0411 | |||
| 9c24d43a13 | |||
| bf9a7ea7d9 | |||
| ca1dbd2fc4 | |||
| 04107ed6d3 | |||
| 209b7bd1aa | |||
| db354875ee | |||
| 40af14b061 | |||
| 1d048e0c35 | |||
| 6a5f467a35 | |||
| 86b0d6d86c | |||
| 565d33f6a7 | |||
| 588e5dd487 | |||
| d8fbf9420e | |||
| edb606814c | |||
| 32823e3760 | |||
| 48f5ffc997 | |||
| 90375e3bc4 | |||
| c4a92ba384 | |||
| 8f7e5d544e | |||
| 40113b0458 | |||
| 48be0a38bf | |||
| 459b327e2d | |||
| d3fde93762 | |||
| aa81c330af | |||
| 7283d112f4 | |||
| f1b0aa2e7c | |||
| 354f3578a2 | |||
| c5ef803458 | |||
| 40801c0d32 | |||
| e9ca17bbd8 | |||
| 5a638ab4b8 | |||
| 7fa4e6f791 | |||
| 61be42bf05 | |||
| 9d4bf22720 | |||
| 24349bed0a | |||
| 17b93322aa | |||
| ef6d92e7af | |||
| 35ed158dd9 | |||
| 31cbcf53a2 | |||
| 3a2fd28199 | |||
| 1473408752 | |||
| 53babfb9fe | |||
| 64694c3a29 | |||
| 1bd2ec0463 | |||
| 4e9625454c | |||
| a58dd35fbe | |||
| 54088f5374 | |||
| 9eebcf9b16 | |||
| 44722a0d4c | |||
| 7eae0a83f9 | |||
| 294b94206f | |||
| 7e73c336dd | |||
| 82d3545c08 | |||
| e312ea943f | |||
| a4a1595ec7 | |||
| 4b07e3a116 | |||
| 13e645fa4b | |||
| 0d876472a3 | |||
| 9e527f4f35 | |||
| eaa5f821a4 | |||
| a495db8480 | |||
| fa99458ca4 | |||
| 1f81e1971b | |||
| 45fc2b62e3 | |||
| f843564444 | |||
| fb216fba8e | |||
| 603cc93957 | |||
| 63e0875f32 | |||
| 029e41472f | |||
| 1752c08fd9 | |||
| 395d9e7650 | |||
| 61d396204f | |||
| f233c511cc | |||
| 0806391ab8 | |||
| dcaba7f186 | |||
| 59dc97b75f | |||
| 85a9ea726c | |||
| d53813adc7 | |||
| 221dd7a81e | |||
| 99bf6349e1 | |||
| 801b902bb8 | |||
| 072b09e030 | |||
| a5680836bd | |||
| bb9ba61d12 | |||
| a88f97831a | |||
| ae0528e5cd | |||
| 74345adf6b | |||
| 7dbee4caed | |||
| f4feb09fbc | |||
| 6cddb3bf82 | |||
| 248e1c6fe9 | |||
| 6e39c79cff | |||
| 5bf4e18ce8 | |||
| cab4a2a8fa |
@@ -2,6 +2,7 @@
|
||||
"name": "Habitica V3 API Documentation",
|
||||
"title": "Habitica",
|
||||
"url": "https://habitica.com",
|
||||
"version": "3.0.0",
|
||||
"sampleUrl": null,
|
||||
"header": {
|
||||
"title": "Introduction",
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as UserModel } from '../../../website/server/models/user';
|
||||
import { TransactionModel } from '../../../website/server/models/transaction';
|
||||
|
||||
const MIGRATION_NAME = '20220915_transactions_user_name';
|
||||
|
||||
/* transaction config */
|
||||
const transactionPerRun = 500;
|
||||
const progressCount = 1000;
|
||||
const transactionQuery = {
|
||||
migration: { $ne: MIGRATION_NAME }, // skip already migrated entries
|
||||
'transactionType': { $in: ['gift_send', 'gift_receive'] },
|
||||
};
|
||||
|
||||
let count = 0;
|
||||
async function updateTransaction (transaction, userNameMap) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (userNameMap.has(transaction.reference)) {
|
||||
set['referenceText'] = userNameMap.get(transaction.reference);
|
||||
} else {
|
||||
set['referenceText'] = 'Account not found';
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) {
|
||||
console.warn(`${count} ${transaction._id}`);
|
||||
}
|
||||
|
||||
return TransactionModel.updateOne({
|
||||
_id: transaction._id
|
||||
}, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processTransactions () {
|
||||
const fields = {
|
||||
_id: 1,
|
||||
reference: 1,
|
||||
referenceText: 1,
|
||||
};
|
||||
|
||||
const userNameMap = new Map();
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const foundTransactions = await TransactionModel // eslint-disable-line no-await-in-loop
|
||||
.find(transactionQuery)
|
||||
.limit(transactionPerRun)
|
||||
.sort({reference: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (foundTransactions.length === 0) {
|
||||
console.warn('All appropriate transactions found and modified.');
|
||||
console.warn(`\n${count} transactions processed\n`);
|
||||
break;
|
||||
}
|
||||
|
||||
// check for unknown users and load the names
|
||||
const userIdsToLoad = [];
|
||||
for (const foundTransaction of foundTransactions) {
|
||||
const userId = foundTransaction.reference;
|
||||
if (userNameMap.has(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
userIdsToLoad.push(userId);
|
||||
}
|
||||
|
||||
const users = await UserModel // eslint-disable-line no-await-in-loop
|
||||
.find({
|
||||
_id: { $in: userIdsToLoad }
|
||||
})
|
||||
.select({
|
||||
_id: 1,
|
||||
'auth.local.username': 1,
|
||||
})
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
for (const user of users) {
|
||||
const localUserName = user.auth?.local?.username;
|
||||
|
||||
if (!localUserName) {
|
||||
console.warn(`\nNo Username found for ID: ${user._id}\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
userNameMap.set(user._id, localUserName)
|
||||
}
|
||||
|
||||
await Promise.all(foundTransactions.map(t => updateTransaction(t, userNameMap))); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import isArray from 'lodash/isArray';
|
||||
import { model as Group } from '../../website/server/models/group';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import * as Tasks from '../../website/server/models/task';
|
||||
|
||||
async function updateTeamTasks (team) {
|
||||
const toSave = [];
|
||||
const teamTasks = await Tasks.Task.find({
|
||||
'group.id': team._id,
|
||||
}).exec();
|
||||
|
||||
const teamBoardTasks = filter(teamTasks, task => !task.userId);
|
||||
const teamUserTasks = filter(teamTasks, task => task.userId);
|
||||
|
||||
for (const boardTask of teamBoardTasks) {
|
||||
if (isArray(boardTask.group.assignedUsers)) {
|
||||
boardTask.group.approval = undefined;
|
||||
boardTask.group.assignedDate = undefined;
|
||||
boardTask.group.assigningUsername = undefined;
|
||||
boardTask.group.sharedCompletion = undefined;
|
||||
|
||||
for (const assignedUserId of boardTask.group.assignedUsers) {
|
||||
const assignedUser = await User.findById(assignedUserId, 'auth'); // eslint-disable-line no-await-in-loop
|
||||
const userTask = find(teamUserTasks, task => task.userId === assignedUserId
|
||||
&& task.group.taskId === boardTask._id);
|
||||
if (!boardTask.group.assignedUsersDetail) boardTask.group.assignedUsersDetail = {};
|
||||
if (userTask && assignedUser) {
|
||||
boardTask.group.assignedUsersDetail[assignedUserId] = {
|
||||
assignedDate: userTask.group.assignedDate,
|
||||
assignedUsername: assignedUser.auth.local.username,
|
||||
assigningUsername: userTask.group.assigningUsername,
|
||||
completed: userTask.completed || false,
|
||||
completedDate: userTask.dateCompleted,
|
||||
};
|
||||
} else if (assignedUser) {
|
||||
boardTask.group.assignedUsersDetail[assignedUserId] = {
|
||||
assignedDate: new Date(),
|
||||
assignedUsername: assignedUser.auth.local.username,
|
||||
assigningUsername: null,
|
||||
completed: false,
|
||||
completedDate: null,
|
||||
};
|
||||
} else {
|
||||
const taskIndex = boardTask.group.assignedUsers.indexOf(assignedUserId);
|
||||
boardTask.group.assignedUsers.splice(taskIndex, 1);
|
||||
}
|
||||
if (userTask) toSave.push(Tasks.Task.findByIdAndDelete(userTask._id));
|
||||
}
|
||||
boardTask.markModified('group');
|
||||
toSave.push(boardTask.save());
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(toSave);
|
||||
}
|
||||
|
||||
export default async function processTeams () {
|
||||
const activeTeams = await Group.find({
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
const taskPromises = activeTeams.map(updateTeamTasks);
|
||||
return Promise.all(taskPromises);
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.237.0",
|
||||
"version": "4.245.2",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@babel/register": "^7.18.6",
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-env": "^7.19.1",
|
||||
"@babel/register": "^7.18.9",
|
||||
"@google-cloud/trace-agent": "^5.1.6",
|
||||
"@parse/node-apn": "^5.1.3",
|
||||
"@slack/webhook": "^6.1.0",
|
||||
"accepts": "^1.3.8",
|
||||
"amazon-payments": "^0.2.9",
|
||||
"amplitude": "^6.0.0",
|
||||
"apidoc": "^0.52.0",
|
||||
"apidoc": "^0.53.0",
|
||||
"apple-auth": "^1.0.7",
|
||||
"bcrypt": "^5.0.1",
|
||||
"body-parser": "^1.20.0",
|
||||
@@ -61,7 +61,7 @@
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.3.7",
|
||||
"rate-limiter-flexible": "^2.3.10",
|
||||
"redis": "^3.1.2",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"remove-markdown": "^0.5.0",
|
||||
@@ -121,7 +121,7 @@
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.3.4",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.6",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon": "^13.0.2",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import forEach from 'lodash/forEach';
|
||||
import { model as Group } from '../website/server/models/group';
|
||||
import { model as User } from '../website/server/models/user';
|
||||
import * as Tasks from '../website/server/models/task';
|
||||
import { daysSince, shouldDo } from '../website/common/script/cron';
|
||||
|
||||
const TASK_VALUE_CHANGE_FACTOR = 0.9747;
|
||||
const MIN_TASK_VALUE = -47.27;
|
||||
|
||||
async function updateTeamTasks (team) {
|
||||
const toSave = [];
|
||||
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
||||
|
||||
if (!teamLeader) { // why would this happen?
|
||||
teamLeader = {
|
||||
preferences: { }, // when options are sanitized this becomes CDS 0 at UTC
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!team.cron || !team.cron.lastProcessed
|
||||
|| daysSince(team.cron.lastProcessed, teamLeader.preferences) > 0
|
||||
) {
|
||||
const tasks = await Tasks.Task.find({
|
||||
'group.id': team._id,
|
||||
userId: { $exists: false },
|
||||
$or: [
|
||||
{ type: 'todo', completed: false },
|
||||
{ type: { $in: ['habit', 'daily'] } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
const tasksByType = {
|
||||
habits: [], dailys: [], todos: [], rewards: [],
|
||||
};
|
||||
forEach(tasks, task => tasksByType[`${task.type}s`].push(task));
|
||||
|
||||
forEach(tasksByType.habits, habit => {
|
||||
if (!(habit.up && habit.down) && habit.value !== 0) {
|
||||
habit.value *= 0.5;
|
||||
if (Math.abs(habit.value) < 0.1) habit.value = 0;
|
||||
toSave.push(habit.save());
|
||||
}
|
||||
});
|
||||
forEach(tasksByType.todos, todo => {
|
||||
if (!todo.completed) {
|
||||
const delta = TASK_VALUE_CHANGE_FACTOR ** todo.value;
|
||||
todo.value -= delta;
|
||||
if (todo.value < MIN_TASK_VALUE) todo.value = MIN_TASK_VALUE;
|
||||
toSave.push(todo.save());
|
||||
}
|
||||
});
|
||||
forEach(tasksByType.dailys, daily => {
|
||||
let processChecklist = false;
|
||||
let assignments = 0;
|
||||
let completions = 0;
|
||||
for (const assignedUser in daily.group.assignedUsersDetail) {
|
||||
if (Object.prototype.hasOwnProperty.call(daily.group.assignedUsersDetail, assignedUser)) {
|
||||
assignments += 1;
|
||||
if (daily.group.assignedUsersDetail[assignedUser].completed) {
|
||||
completions += 1;
|
||||
daily.group.assignedUsersDetail[assignedUser].completed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (completions > 0) daily.markModified('group.assignedUsersDetail');
|
||||
if (daily.completed) {
|
||||
processChecklist = true;
|
||||
daily.completed = false;
|
||||
} else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) {
|
||||
processChecklist = true;
|
||||
const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value;
|
||||
if (assignments > 0) {
|
||||
daily.value -= ((completions / assignments) * delta);
|
||||
}
|
||||
if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE;
|
||||
}
|
||||
daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences);
|
||||
if (processChecklist && daily.checklist.length > 0) {
|
||||
daily.checklist.forEach(i => { i.completed = false; });
|
||||
}
|
||||
toSave.push(daily.save());
|
||||
});
|
||||
|
||||
if (!team.cron) team.cron = {};
|
||||
team.cron.lastProcessed = new Date();
|
||||
toSave.push(team.save());
|
||||
}
|
||||
|
||||
return Promise.all(toSave);
|
||||
}
|
||||
|
||||
export default async function processTeamsCron () {
|
||||
const activeTeams = await Group.find({
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
const cronPromises = activeTeams.map(updateTeamTasks);
|
||||
return Promise.all(cronPromises);
|
||||
}
|
||||
@@ -13,11 +13,6 @@ function getUser () {
|
||||
username: 'username',
|
||||
email: 'email@email',
|
||||
},
|
||||
facebook: {
|
||||
emails: [{
|
||||
value: 'email@facebook',
|
||||
}],
|
||||
},
|
||||
google: {
|
||||
emails: [{
|
||||
value: 'email@google',
|
||||
@@ -62,30 +57,12 @@ describe('emails', () => {
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [facebook users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.auth.local.username);
|
||||
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [google users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
@@ -103,7 +80,6 @@ describe('emails', () => {
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.facebook.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
@@ -118,7 +94,6 @@ describe('emails', () => {
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook;
|
||||
delete user.auth.google;
|
||||
delete user.auth.apple;
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ describe('Password Utilities', () => {
|
||||
it('returns false if the user has no local auth', async () => {
|
||||
const user = await generateUser({
|
||||
auth: {
|
||||
facebook: {},
|
||||
google: {},
|
||||
},
|
||||
});
|
||||
const res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
|
||||
@@ -672,10 +672,12 @@ describe('payments/index', () => {
|
||||
context('No Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(null);
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
worldState.getCurrentEventList.restore();
|
||||
});
|
||||
|
||||
it('does not apply a discount', async () => {
|
||||
@@ -692,14 +694,14 @@ describe('payments/index', () => {
|
||||
|
||||
context('Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns({
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([{
|
||||
...common.content.events.fall2020,
|
||||
event: 'fall2020',
|
||||
});
|
||||
}]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
worldState.getCurrentEventList.restore();
|
||||
});
|
||||
|
||||
it('applies a discount', async () => {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { each, find, findIndex } from 'lodash';
|
||||
import { model as Challenge } from '../../../../website/server/models/challenge';
|
||||
import { each, findIndex } from 'lodash';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
|
||||
describe('Group Task Methods', () => {
|
||||
let guild; let leader; let challenge; let
|
||||
task;
|
||||
let guild; let leader; let task;
|
||||
const tasksToTest = {
|
||||
habit: {
|
||||
text: 'test habit',
|
||||
@@ -31,10 +29,6 @@ describe('Group Task Methods', () => {
|
||||
},
|
||||
};
|
||||
|
||||
function findLinkedTask (updatedLeadersTask) {
|
||||
return updatedLeadersTask.group.taskId === task._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
guild = new Group({
|
||||
name: 'test party',
|
||||
@@ -47,19 +41,9 @@ describe('Group Task Methods', () => {
|
||||
|
||||
guild.leader = leader._id;
|
||||
|
||||
challenge = new Challenge({
|
||||
name: 'Test Challenge',
|
||||
shortName: 'Test',
|
||||
leader: leader._id,
|
||||
group: guild._id,
|
||||
});
|
||||
|
||||
leader.challenges = [challenge._id];
|
||||
|
||||
await Promise.all([
|
||||
guild.save(),
|
||||
leader.save(),
|
||||
challenge.save(),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -78,7 +62,15 @@ describe('Group Task Methods', () => {
|
||||
});
|
||||
|
||||
it('syncs an assigned task to a user', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.syncTask(task, [leader], leader);
|
||||
|
||||
const updatedTask = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(updatedTask.group.assignedUsers).to.contain(leader._id);
|
||||
expect(updatedTask.group.assignedUsersDetail[leader._id]).to.exist;
|
||||
});
|
||||
|
||||
it('creates tags for a user when task is synced', async () => {
|
||||
await guild.syncTask(task, [leader], leader);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const tagIndex = findIndex(updatedLeader.tags, { id: guild._id });
|
||||
@@ -88,197 +80,6 @@ describe('Group Task Methods', () => {
|
||||
expect(newTag.name).to.equal(guild.name);
|
||||
expect(newTag.group).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('create tags for a user when task is synced', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('syncs updated info for assigned task to a user', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
const updatedTaskName = 'Update Task name';
|
||||
task.text = updatedTaskName;
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
});
|
||||
|
||||
it('syncs checklist items to an assigned user', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
|
||||
});
|
||||
|
||||
describe('syncs updated info', async () => {
|
||||
let newMember;
|
||||
|
||||
beforeEach(async () => {
|
||||
newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.syncTask(task, newMember);
|
||||
});
|
||||
|
||||
it('syncs updated info for assigned task to all users', async () => {
|
||||
const updatedTaskName = 'Update Task name';
|
||||
task.text = updatedTaskName;
|
||||
task.group.approval.required = true;
|
||||
|
||||
await guild.updateTask(task);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
const updatedMember = await User.findOne({ _id: newMember._id });
|
||||
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
|
||||
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
expect(syncedTask.group.approval.required).to.equal(true);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(newMember._id);
|
||||
expect(syncedMemberTask).to.exist;
|
||||
expect(syncedMemberTask.text).to.equal(task.text);
|
||||
expect(syncedMemberTask.group.approval.required).to.equal(true);
|
||||
});
|
||||
|
||||
it('syncs a new checklist item to all assigned users', async () => {
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
const newCheckListItem = {
|
||||
text: 'Checklist Item 1',
|
||||
completed: false,
|
||||
};
|
||||
|
||||
task.checklist.push(newCheckListItem);
|
||||
|
||||
await guild.updateTask(task, { newCheckListItem });
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
const updatedMember = await User.findOne({ _id: newMember._id });
|
||||
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
|
||||
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
|
||||
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
|
||||
});
|
||||
|
||||
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
const updateCheckListText = 'Updated checklist item';
|
||||
if (task.checklist) {
|
||||
task.checklist[0].text = updateCheckListText;
|
||||
}
|
||||
|
||||
await guild.updateTask(task, { updateCheckListItems: [task.checklist[0]] });
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
const updatedMember = await User.findOne({ _id: newMember._id });
|
||||
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
|
||||
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
|
||||
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
|
||||
});
|
||||
|
||||
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
await guild.updateTask(task, { removedCheckListItemId: task.checklist[0].id });
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
const updatedMember = await User.findOne({ _id: newMember._id });
|
||||
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
|
||||
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.checklist.length).to.equal(0);
|
||||
expect(syncedMemberTask.checklist.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('removes assigned tasks when master task is deleted', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.removeTask(task);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.unlinkTask(task, leader, 'remove-all');
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.not.contain(leader._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unlinks and keeps group tasks for a user when keep-all is specified', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
let updatedLeader = await User.findOne({ _id: leader._id });
|
||||
let updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
await guild.unlinkTask(task, leader, 'keep-all');
|
||||
|
||||
updatedLeader = await User.findOne({ _id: leader._id });
|
||||
updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const updatedSyncedTask = find(
|
||||
updatedLeadersTasks,
|
||||
updatedLeadersTask => updatedLeadersTask._id === syncedTask._id,
|
||||
);
|
||||
|
||||
expect(task.group.assignedUsers).to.not.contain(leader._id);
|
||||
expect(updatedSyncedTask).to.exist;
|
||||
expect(updatedSyncedTask.group._id).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,13 +246,23 @@ describe('Task Model', () => {
|
||||
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
|
||||
});
|
||||
|
||||
it('scopes alias lookup to user', async () => {
|
||||
it('scopes alias lookup to user when querying aliases only', async () => {
|
||||
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id);
|
||||
|
||||
expect(Tasks.Task.find).to.be.calledOnce;
|
||||
expect(Tasks.Task.find).to.be.calledWithMatch({
|
||||
alias: { $in: [taskWithAlias.alias] },
|
||||
userId: user._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('scopes alias lookup to user when querying aliases and IDs', async () => {
|
||||
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias, secondTask._id], user._id);
|
||||
|
||||
expect(Tasks.Task.find).to.be.calledOnce;
|
||||
expect(Tasks.Task.find).to.be.calledWithMatch({
|
||||
$or: [
|
||||
{ _id: { $in: [] } },
|
||||
{ _id: { $in: [secondTask._id] } },
|
||||
{ alias: { $in: [taskWithAlias.alias] } },
|
||||
],
|
||||
userId: user._id,
|
||||
@@ -270,10 +280,7 @@ describe('Task Model', () => {
|
||||
|
||||
expect(Tasks.Task.find).to.be.calledOnce;
|
||||
expect(Tasks.Task.find).to.be.calledWithMatch({
|
||||
$or: [
|
||||
{ _id: { $in: [] } },
|
||||
{ alias: { $in: [taskWithAlias.alias] } },
|
||||
],
|
||||
alias: { $in: [taskWithAlias.alias] },
|
||||
userId: user._id,
|
||||
foo: 'bar',
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('POST /challenges', () => {
|
||||
it('returns error when group is empty', async () => {
|
||||
@@ -60,6 +61,22 @@ describe('POST /challenges', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
|
||||
const user = await generateUser();
|
||||
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
|
||||
const group = createAndPopulateGroup({
|
||||
members: 1,
|
||||
});
|
||||
await expect(user.post('/challenges', {
|
||||
group: group._id,
|
||||
summary,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
context('Creating a challenge for a valid group', () => {
|
||||
let groupLeader;
|
||||
let group;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('PUT /challenges/:challengeId', () => {
|
||||
let privateGuild; let user; let nonMember; let challenge; let
|
||||
@@ -91,4 +92,15 @@ describe('PUT /challenges/:challengeId', () => {
|
||||
expect(res.name).to.equal('New Challenge Name');
|
||||
expect(res.description).to.equal('New challenge description.');
|
||||
});
|
||||
|
||||
it('return error when challenge summary is greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
|
||||
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
|
||||
await expect(user.put(`/challenges/${challenge._id}`, {
|
||||
summary,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { find } from 'lodash';
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
@@ -11,10 +10,6 @@ describe('POST /group/:groupId/remove-manager', () => {
|
||||
const groupType = 'guild';
|
||||
let nonManager;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === groupToUpdate._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
@@ -63,28 +58,4 @@ describe('POST /group/:groupId/remove-manager', () => {
|
||||
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes group approval notifications from a manager that is removed', async () => {
|
||||
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
const task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`);
|
||||
const memberTasks = await nonManager.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await nonManager.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
await nonLeader.sync();
|
||||
|
||||
expect(nonLeader.notifications.length).to.equal(1); // user gets mystery items
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('POST /group', () => {
|
||||
let user;
|
||||
@@ -71,6 +72,20 @@ describe('POST /group', () => {
|
||||
|
||||
expect(updatedGroup.summary).to.eql(summary);
|
||||
});
|
||||
|
||||
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
|
||||
const name = 'Test Group';
|
||||
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
|
||||
await expect(user.post('/groups', {
|
||||
name,
|
||||
type: 'guild',
|
||||
summary,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Guilds', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('PUT /group', () => {
|
||||
let leader; let nonLeader; let groupToUpdate; let
|
||||
@@ -130,4 +131,15 @@ describe('PUT /group', () => {
|
||||
|
||||
expect(response.bannedWordsAllowed).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
|
||||
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
|
||||
await expect(leader.put(`/groups/${groupToUpdate._id}`, {
|
||||
summary,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ describe('POST /tasks/clearCompletedTodos', () => {
|
||||
{ 'purchased.plan.customerId': 'group-unlimited' },
|
||||
);
|
||||
const challenge = await generateChallenge(user, guild);
|
||||
await user.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
const initialTodoCount = user.tasksOrder.todos.length;
|
||||
@@ -33,7 +36,7 @@ describe('POST /tasks/clearCompletedTodos', () => {
|
||||
text: 'todo 7',
|
||||
type: 'todo',
|
||||
});
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
|
||||
|
||||
const tasks = await user.get('/tasks/user?type=todos');
|
||||
expect(tasks.length).to.equal(initialTodoCount + 7);
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
|
||||
expect(savedTask.checklist[0].completed).to.equal(true);
|
||||
});
|
||||
|
||||
it('can use a alias to score a checklist item', async () => {
|
||||
it('can use an alias to score a checklist item', async () => {
|
||||
const task = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { find } from 'lodash';
|
||||
import {
|
||||
translate as t,
|
||||
createAndPopulateGroup,
|
||||
@@ -8,10 +7,6 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
let user; let guild; let member; let member2; let
|
||||
task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, members, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
@@ -35,8 +30,7 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
|
||||
});
|
||||
|
||||
it('deletes a group task', async () => {
|
||||
@@ -64,81 +58,4 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
message: t('messageTaskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('removes deleted taskʾs approval pending notifications from managers', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
await user.put(`/tasks/${task._id}/`, {
|
||||
requiresApproval: true,
|
||||
});
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(3); // mystery items
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(3);
|
||||
expect(member2.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
await member2.del(`/tasks/${task._id}`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
});
|
||||
|
||||
it('deletes task from assigned user', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('deletes task from all assigned users', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask).to.not.exist;
|
||||
expect(member2SyncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('prevents a user from deleting a task they are assigned to', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.del(`/tasks/${syncedTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDeleteAssignedGroupTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to delete a task after leaving a group', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.post(`/groups/${guild._id}/leave`);
|
||||
|
||||
await member.del(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await expect(member.get(`/tasks/${syncedTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Task not found.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { find } from 'lodash';
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('GET /approvals/group/:groupId', () => {
|
||||
let user; let guild; let member; let addlMember; let task; let syncedTask; let
|
||||
addlSyncedTask;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, members, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0]; // eslint-disable-line prefer-destructuring
|
||||
addlMember = members[1]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign/${addlMember._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
const addlMemberTasks = await addlMember.get('/tasks/user');
|
||||
addlSyncedTask = find(addlMemberTasks, findAssignedTask);
|
||||
|
||||
try {
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-empty
|
||||
}
|
||||
|
||||
try {
|
||||
await addlMember.post(`/tasks/${addlSyncedTask._id}/score/up`);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-empty
|
||||
}
|
||||
});
|
||||
|
||||
it('provides only user\'s own tasks when user is not the group leader', async () => {
|
||||
const approvals = await member.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
expect(approvals[1]).to.not.exist;
|
||||
});
|
||||
|
||||
it('allows group leaders to get a list of tasks that need approval', async () => {
|
||||
const approvals = await user.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
|
||||
});
|
||||
|
||||
it('allows managers to get a list of tasks that need approval', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member._id,
|
||||
});
|
||||
|
||||
const approvals = await member.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
|
||||
});
|
||||
});
|
||||
@@ -1,261 +0,0 @@
|
||||
import { find } from 'lodash';
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let user; let guild; let member; let member2; let
|
||||
task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, members, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0]; // eslint-disable-line prefer-destructuring
|
||||
member2 = members[1]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user is not assigned', async () => {
|
||||
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageTaskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user is not the group leader', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await expect(member.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('approves an assigned user', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.approval.approved).to.be.true;
|
||||
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('allows a manager to approve an assigned user', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.approval.approved).to.be.true;
|
||||
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
|
||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('removes approval pending notifications from managers', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('prevents double approval on a task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('canOnlyApproveTaskOnce'),
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents approving a task if it is not waiting for approval', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalWasNotRequested'),
|
||||
});
|
||||
});
|
||||
|
||||
it('completes master task when single-completion task is approved', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
|
||||
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
});
|
||||
|
||||
it('deletes other assigned user tasks when single-completion task is approved', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
|
||||
const syncedTask2 = find(
|
||||
member2Tasks,
|
||||
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
|
||||
);
|
||||
|
||||
expect(syncedTask2).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
|
||||
|
||||
expect(masterTask.completed).to.equal(false);
|
||||
});
|
||||
|
||||
it('completes master task when all user tasks are approved if all assigned must complete', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
await member2.post(`/tasks/${member2SyncedTask._id}/score/up`);
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
|
||||
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { find } from 'lodash';
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
@@ -8,10 +7,6 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
let user; let guild; let member; let member2; let
|
||||
task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, members, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
@@ -37,14 +32,15 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
it('errors when user is not assigned', async () => {
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageTaskNotFound'),
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Task not completed by this user.',
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user is not the group leader', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
await member.post(`/tasks/${task._id}/score/up`);
|
||||
await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
@@ -54,132 +50,64 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
});
|
||||
|
||||
it('marks a task as needing more work', async () => {
|
||||
await member.sync();
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
|
||||
// score task to require approval
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member.post(`/tasks/${task._id}/score/up`);
|
||||
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// Check that the notification approval request has been removed
|
||||
expect(syncedTask.group.approval.requested).to.equal(false);
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
// Check that the notification is correct
|
||||
await member.sync();
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 3);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
const taskText = syncedTask.text;
|
||||
const managerName = user.profile.name;
|
||||
const taskText = task.text;
|
||||
const managerName = user.auth.local.username;
|
||||
|
||||
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
|
||||
|
||||
expect(notification.data.task.id).to.equal(syncedTask._id);
|
||||
expect(notification.data.task.id).to.equal(task._id);
|
||||
expect(notification.data.task.text).to.equal(taskText);
|
||||
|
||||
expect(notification.data.group.id).to.equal(syncedTask.group.id);
|
||||
expect(notification.data.group.id).to.equal(task.group.id);
|
||||
expect(notification.data.group.name).to.equal(guild.name);
|
||||
|
||||
expect(notification.data.manager.id).to.equal(user._id);
|
||||
expect(notification.data.manager.name).to.equal(managerName);
|
||||
|
||||
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
|
||||
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('allows a manager to mark a task as needing work', async () => {
|
||||
await member.sync();
|
||||
const initialNotifications = member.notifications.length;
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
|
||||
// score task to require approval
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
await member.post(`/tasks/${task._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// Check that the notification approval request has been removed
|
||||
expect(syncedTask.group.approval.requested).to.equal(false);
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
await member.sync();
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 3);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
const taskText = syncedTask.text;
|
||||
const managerName = member2.profile.name;
|
||||
const taskText = task.text;
|
||||
const managerName = member2.auth.local.username;
|
||||
|
||||
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
|
||||
|
||||
expect(notification.data.task.id).to.equal(syncedTask._id);
|
||||
expect(notification.data.task.id).to.equal(task._id);
|
||||
expect(notification.data.task.text).to.equal(taskText);
|
||||
|
||||
expect(notification.data.group.id).to.equal(syncedTask.group.id);
|
||||
expect(notification.data.group.id).to.equal(task.group.id);
|
||||
expect(notification.data.group.name).to.equal(guild.name);
|
||||
|
||||
expect(notification.data.manager.id).to.equal(member2._id);
|
||||
expect(notification.data.manager.name).to.equal(managerName);
|
||||
|
||||
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
|
||||
await Promise.all([user.sync(), member2.sync()]);
|
||||
|
||||
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
|
||||
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
|
||||
expect(member2.notifications.find(n => { // eslint-disable-line arrow-body-style
|
||||
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('prevents marking a task as needing work if it was already approved', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('canOnlyApproveTaskOnce'),
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents marking a task as needing work if it is not waiting for approval', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalWasNotRequested'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,10 +8,6 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
let user; let guild; let member; let member2; let
|
||||
task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, members, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
@@ -30,209 +26,50 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
});
|
||||
|
||||
it('prevents user from scoring a task that needs to be approved', async () => {
|
||||
await user.update({
|
||||
'preferences.language': 'cs',
|
||||
});
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
|
||||
|
||||
expect(response.data.requiresApproval).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
|
||||
|
||||
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[2].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(updatedTask.group.approval.requested).to.equal(true);
|
||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('sends notifications to all managers', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
|
||||
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(user.notifications[2].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('errors when approval has already been requested', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const response = await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
expect(response.data.requiresApproval).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskRequiresApproval'));
|
||||
});
|
||||
|
||||
it('allows a user to score an approved task', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
|
||||
expect(updatedTask.completed).to.equal(true);
|
||||
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('completes master task when single-completion task is completed', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
|
||||
const syncedTask = find(
|
||||
memberTasks,
|
||||
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
|
||||
);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
it('completes single-assigned task', async () => {
|
||||
await member.post(`/tasks/${task._id}/score/up`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
|
||||
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
expect(sourceTask.completed).to.equal(true);
|
||||
});
|
||||
|
||||
it('deletes other assigned user tasks when single-completion task is completed', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
it('errors when task has already been completed', async () => {
|
||||
await member.post(`/tasks/${task._id}/score/up`);
|
||||
|
||||
await expect(member.post(`/tasks/${task._id}/score/up`)).to.be.rejected.and.to.eventually.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
|
||||
const syncedTask = find(
|
||||
memberTasks,
|
||||
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
|
||||
);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
|
||||
const syncedTask2 = find(
|
||||
member2Tasks,
|
||||
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
|
||||
);
|
||||
|
||||
expect(syncedTask2).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
it('does not complete multi-assigned task when not all assignees have completed', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
|
||||
const syncedTask = find(
|
||||
memberTasks,
|
||||
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
|
||||
);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member.post(`/tasks/${task._id}/score/up`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
|
||||
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
|
||||
|
||||
expect(masterTask.completed).to.equal(false);
|
||||
expect(sourceTask.completed).to.equal(false);
|
||||
});
|
||||
|
||||
it('completes master task when all user tasks are completed if all assigned must complete', async () => {
|
||||
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
it('completes multi-assigned task when all assignees have completed', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const syncedTask = find(
|
||||
memberTasks,
|
||||
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
|
||||
);
|
||||
const syncedTask2 = find(
|
||||
member2Tasks,
|
||||
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
|
||||
);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${syncedTask2._id}/score/up`);
|
||||
await member.post(`/tasks/${task._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/score/up`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
|
||||
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
expect(sourceTask.completed).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
});
|
||||
|
||||
it('returns error when task is not found', async () => {
|
||||
await expect(user.post(`/tasks/${generateUUID()}/assign/${member._id}`))
|
||||
await expect(user.post(`/tasks/${generateUUID()}/assign`, [member._id]))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
@@ -56,7 +56,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${nonGroupTask._id}/assign/${member._id}`))
|
||||
await expect(user.post(`/tasks/${nonGroupTask._id}/assign`, [member._id]))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -67,7 +67,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
it('returns error when user is not a member of the group', async () => {
|
||||
const nonUser = await generateUser();
|
||||
|
||||
await expect(nonUser.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
await expect(nonUser.post(`/tasks/${task._id}/assign`, [member._id]))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
@@ -76,7 +76,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
await expect(member2.post(`/tasks/${task._id}/assign`, [member._id]))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -84,49 +84,23 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows user to assign themselves (claim)', async () => {
|
||||
await member.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('sends notifications to group leader and managers when a task is claimed', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
await member.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(user.notifications.length).to.equal(3); // includes Guild Joined achievement
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_CLAIMED');
|
||||
expect(user.notifications[2].data.taskId).to.equal(groupTask[0]._id);
|
||||
expect(user.notifications[2].data.groupId).to.equal(guild._id);
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
expect(member2.notifications[1].type).to.equal('GROUP_TASK_CLAIMED');
|
||||
expect(member2.notifications[1].data.taskId).to.equal(groupTask[0]._id);
|
||||
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('assigns a task to a user', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
await member.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('sends a notification to assigned user', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
await member.sync();
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
@@ -137,20 +111,27 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
});
|
||||
|
||||
it('assigns a task to multiple users', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
await member.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const member1SyncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member2.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
||||
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
|
||||
expect(member1SyncedTask).to.exist;
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
||||
expect(groupTask[0].group.assignedUsersDetail[member2._id]).to.exist;
|
||||
expect(member2SyncedTask).to.exist;
|
||||
});
|
||||
|
||||
@@ -159,13 +140,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
|
||||
const groupTask = await member2.get(`/tasks/group/${guild._id}`);
|
||||
await member.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member._id]);
|
||||
});
|
||||
|
||||
it('returns error when task is not found', async () => {
|
||||
@@ -96,7 +96,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
});
|
||||
|
||||
it('unassigns a user and only that user from a task', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
|
||||
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
@@ -105,6 +105,9 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const member1SyncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member2.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
@@ -130,20 +133,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('allows a user to unassign themselves', async () => {
|
||||
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
|
||||
// here, but then we had a request to allow to unaissgn.
|
||||
xit('returns error when non leader tries to unassign their a task', async () => {
|
||||
it('returns error when non leader tries to unassign a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { find } from 'lodash';
|
||||
import {
|
||||
createAndPopulateGroup, translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
@@ -11,10 +10,6 @@ describe('PUT /tasks/:id', () => {
|
||||
let habit;
|
||||
let todo;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, members, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
@@ -44,8 +39,7 @@ describe('PUT /tasks/:id', () => {
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${habit._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${habit._id}/assign`, [member._id, member2._id]);
|
||||
});
|
||||
|
||||
it('updates a group task', async () => {
|
||||
@@ -56,28 +50,6 @@ describe('PUT /tasks/:id', () => {
|
||||
expect(savedHabit.notes).to.eql('some new notes');
|
||||
});
|
||||
|
||||
it('updates a group task - approval is required', async () => {
|
||||
// allow to manage
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member._id,
|
||||
});
|
||||
|
||||
// change the habit
|
||||
habit = await member.put(`/tasks/${habit._id}`, {
|
||||
text: 'new text!',
|
||||
requiresApproval: true,
|
||||
});
|
||||
|
||||
const memberTasks = await member2.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id);
|
||||
|
||||
// score up to trigger approval
|
||||
const response = await member2.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
expect(response.data.requiresApproval).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
|
||||
});
|
||||
|
||||
it('member updates a group task value - not allowed', async () => {
|
||||
// change the todo
|
||||
await expect(member.put(`/tasks/${habit._id}`, {
|
||||
@@ -120,7 +92,7 @@ describe('PUT /tasks/:id', () => {
|
||||
],
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${habit._id}/assign`, [member._id]);
|
||||
|
||||
// change the checklist text
|
||||
habit = await user.put(`/tasks/${habit._id}`, {
|
||||
@@ -137,63 +109,4 @@ describe('PUT /tasks/:id', () => {
|
||||
|
||||
expect(habit.checklist.length).to.eql(2);
|
||||
});
|
||||
|
||||
it('updates the linked tasks', async () => {
|
||||
await user.put(`/tasks/${habit._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.text).to.eql('some new text');
|
||||
expect(syncedTask.up).to.eql(false);
|
||||
expect(syncedTask.down).to.eql(false);
|
||||
});
|
||||
|
||||
it('updates the linked tasks for all assigned users', async () => {
|
||||
await user.put(`/tasks/${habit._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.text).to.eql('some new text');
|
||||
expect(syncedTask.up).to.eql(false);
|
||||
expect(syncedTask.down).to.eql(false);
|
||||
|
||||
expect(member2SyncedTask.text).to.eql('some new text');
|
||||
expect(member2SyncedTask.up).to.eql(false);
|
||||
expect(member2SyncedTask.down).to.eql(false);
|
||||
});
|
||||
|
||||
it('updates the linked tasks', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.put(`/tasks/${habit._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.text).to.eql('some new text');
|
||||
expect(syncedTask.up).to.eql(false);
|
||||
expect(syncedTask.down).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -289,45 +289,6 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('user with Facebook auth', async () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
auth: {
|
||||
facebook: {
|
||||
id: 'facebook-id',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if confirmation phrase is wrong', async () => {
|
||||
await expect(user.del('/user', {
|
||||
password: 'just-do-it',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('incorrectDeletePhrase', { magicWord: 'DELETE' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if confirmation phrase is not supplied', async () => {
|
||||
await expect(user.del('/user', {
|
||||
password: '',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingPassword'),
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a Facebook user', async () => {
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('user with Google auth', async () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
|
||||
@@ -145,19 +145,16 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
|
||||
const memberTasks = await groupLeader.get('/tasks/user');
|
||||
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id);
|
||||
|
||||
await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]);
|
||||
await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 });
|
||||
await sleep(0.5);
|
||||
await groupLeader.sync();
|
||||
|
||||
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
|
||||
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('groupTasksNoCast'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageTaskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -279,7 +276,10 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
type: 'todo',
|
||||
});
|
||||
await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 });
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
|
||||
await user.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
|
||||
await user.post('/user/class/cast/brightness');
|
||||
await user.sync();
|
||||
|
||||
@@ -100,11 +100,14 @@ describe('POST /user/reset', () => {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.sync();
|
||||
|
||||
await user.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
const memberTasks = await user.get('/tasks/user');
|
||||
|
||||
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id);
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('PUT /user', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'mustBeArray',
|
||||
message: 'Tag list must be an array.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,44 +20,6 @@ describe('DELETE social registration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('Facebook', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
await expect(user.del('/user/auth/social/facebook')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDetachSocial'),
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds if user has a local registration', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/facebook');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.facebook).to.be.undefined;
|
||||
});
|
||||
|
||||
it('succeeds if user has a google registration', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/facebook');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.facebook).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
context('Google', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
@@ -81,19 +43,6 @@ describe('DELETE social registration', () => {
|
||||
await user.sync();
|
||||
expect(user.auth.google).to.be.undefined;
|
||||
});
|
||||
|
||||
it('succeeds if user has a facebook registration', async () => {
|
||||
await user.update({
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.facebook.id': 'some-facebook-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/google');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.goodl).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
context('Apple', () => {
|
||||
@@ -119,18 +68,5 @@ describe('DELETE social registration', () => {
|
||||
await user.sync();
|
||||
expect(user.auth.apple).to.be.undefined;
|
||||
});
|
||||
|
||||
it('succeeds if user has a facebook registration', async () => {
|
||||
await user.update({
|
||||
'auth.apple.id': 'some-apple-id',
|
||||
'auth.facebook.id': 'some-facebook-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/apple');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.goodl).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ describe('POST /user/auth/social', () => {
|
||||
let user;
|
||||
const endpoint = '/user/auth/social';
|
||||
let randomAccessToken = '123456';
|
||||
let randomFacebookId = 'facebookId';
|
||||
let randomGoogleId = 'googleId';
|
||||
let network = 'NoNetwork';
|
||||
|
||||
@@ -33,146 +32,6 @@ describe('POST /user/auth/social', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('facebook', () => {
|
||||
beforeEach(async () => {
|
||||
randomFacebookId = generateUUID();
|
||||
const expectedResult = {
|
||||
id: randomFacebookId,
|
||||
displayName: 'a facebook user',
|
||||
emails: [
|
||||
{ value: `${user.auth.local.username}+facebook@example.com` },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
network = 'facebook';
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
passport._strategies.facebook.userProfile.restore();
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
expect(response.username).to.exist;
|
||||
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
|
||||
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+facebook@example.com`);
|
||||
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(randomFacebookId);
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
});
|
||||
|
||||
it('logs an existing user in if they have local auth with matching email', async () => {
|
||||
passport._strategies.facebook.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomFacebookId,
|
||||
displayName: 'a facebook user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('logs an existing user into their social account if they have local auth with matching email', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
// This is important for existing accounts before the new social handling
|
||||
passport._strategies.facebook.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomFacebookId,
|
||||
displayName: 'a facebook user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.apiToken).not.to.eql(user.apiToken);
|
||||
expect(response.id).not.to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
const response = await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('does not log into other account if social auth already exists', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
|
||||
await expect(user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('socialAlreadyExists'),
|
||||
});
|
||||
});
|
||||
|
||||
xit('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('google', () => {
|
||||
beforeEach(async () => {
|
||||
randomGoogleId = generateUUID();
|
||||
|
||||
@@ -25,6 +25,19 @@ describe('POST /user/reset-password', async () => {
|
||||
expect(user.auth.local.hashed_password).to.not.eql(previousPassword);
|
||||
});
|
||||
|
||||
it('resets password for social users', async () => {
|
||||
const email = `${user.auth.local.username}+google@example.com`;
|
||||
await user.update({ 'auth.google.emails': [{ value: email }] });
|
||||
await user.sync();
|
||||
const previousPassword = user.auth.local.passwordResetCode;
|
||||
const response = await user.post(endpoint, {
|
||||
email,
|
||||
});
|
||||
expect(response).to.eql({ data: {}, message: t('passwordReset') });
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordResetCode).to.not.eql(previousPassword);
|
||||
});
|
||||
|
||||
it('same message on error as on success', async () => {
|
||||
const response = await user.post(endpoint, {
|
||||
email: 'nonExistent@email.com',
|
||||
|
||||
@@ -130,19 +130,16 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
|
||||
const memberTasks = await groupLeader.get('/tasks/user');
|
||||
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id);
|
||||
|
||||
await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]);
|
||||
await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 });
|
||||
await sleep(0.5);
|
||||
await groupLeader.sync();
|
||||
|
||||
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
|
||||
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('groupTasksNoCast'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageTaskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -247,7 +244,10 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
type: 'todo',
|
||||
});
|
||||
await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 });
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
|
||||
await user.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
|
||||
await user.post('/user/class/cast/brightness');
|
||||
await user.sync();
|
||||
|
||||
@@ -100,11 +100,14 @@ describe('POST /user/reset', () => {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.sync();
|
||||
|
||||
await user.put('/user', {
|
||||
'preferences.tasks.mirrorGroupTasks': [guild._id],
|
||||
});
|
||||
const memberTasks = await user.get('/tasks/user');
|
||||
|
||||
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id);
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('PUT /user', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'mustBeArray',
|
||||
message: 'Tag list must be an array.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ describe('cron utility functions', () => {
|
||||
};
|
||||
}
|
||||
|
||||
it('offset 0, next date in 3 months', () => {
|
||||
it('monthly plan, next date in 3 months', () => {
|
||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
@@ -222,8 +222,8 @@ describe('cron utility functions', () => {
|
||||
.to.be.sameMoment('2022-08-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('offset 1, next date in 1 months', () => {
|
||||
const user = baseUserData(60, 1, 'group_plan_auto');
|
||||
it('monthly plan, next date in 1 month', () => {
|
||||
const user = baseUserData(62, 0, 'group_plan_auto');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
@@ -231,8 +231,17 @@ describe('cron utility functions', () => {
|
||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('offset 2, next date in 2 months - with any plan', () => {
|
||||
const user = baseUserData(60, 2, 'basic_3mo');
|
||||
it('multi-month plan, no offset', () => {
|
||||
const user = baseUserData(60, 0, 'basic_3mo');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan with offset', () => {
|
||||
const user = baseUserData(60, 1, 'basic_3mo');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
|
||||
@@ -249,18 +249,6 @@ describe('shared.ops.scoreTask', () => {
|
||||
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
|
||||
});
|
||||
|
||||
it('does not modify stats when task need approval', () => {
|
||||
todo.group.approval.required = true;
|
||||
options = {
|
||||
user: ref.afterUser, task: todo, direction: 'up', times: 5, cron: false,
|
||||
};
|
||||
scoreTask(options);
|
||||
|
||||
expect(ref.afterUser.stats.hp).to.eql(50);
|
||||
expect(ref.afterUser.stats.exp).to.equal(ref.beforeUser.stats.exp);
|
||||
expect(ref.afterUser.stats.gp).to.equal(ref.beforeUser.stats.gp);
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
it('up', () => {
|
||||
options = {
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
}
|
||||
},
|
||||
"@amplitude/types": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.0.tgz",
|
||||
"integrity": "sha512-xN0gnhutztv6kqHaZ2bre18anQV5GDmMXOeipTvI670g2VjNbPfOzMwu1LN4p1NadYq+GqYI223UcZrXR+R4Pw=="
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.2.tgz",
|
||||
"integrity": "sha512-I8qenRI7uU6wKNb9LiZrAosSHVoNHziXouKY81CrqxH9xhVTEIJFXeuCV0hbtBr0Al/8ejnGjQRx+S2SvU/pPg=="
|
||||
},
|
||||
"@amplitude/ua-parser-js": {
|
||||
"version": "0.7.31",
|
||||
@@ -40,12 +40,19 @@
|
||||
"integrity": "sha512-+z8UGRaj13Pt5NDzOnkTBy49HE2CX64jeL0ArB86HAtilpnfkPB7oqkigN7Lf2LxscMg4QhFD7mmCfedh3rqTg=="
|
||||
},
|
||||
"@amplitude/utils": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.0.tgz",
|
||||
"integrity": "sha512-/R8j8IzFH0GYfA6ehQDm5IEzt71gIeMdiYYFIzZp6grERQlgJcwNJMAiza0o2JwwTDIruzqdB3c/vLVjuakp+w==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.2.tgz",
|
||||
"integrity": "sha512-tVsHXu61jITEtRjB7NugQ5cVDd4QDzne8T3ifmZye7TiJeUfVRvqe44gDtf55A+7VqhDhyEIIXTA1iVcDGqlEw==",
|
||||
"requires": {
|
||||
"@amplitude/types": "^1.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
"@amplitude/types": "^1.10.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
@@ -1799,26 +1806,26 @@
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-optional-chaining": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.6.tgz",
|
||||
"integrity": "sha512-PatI6elL5eMzoypFAiYDpYQyMtXTn+iMhuxxQt5mAXD4fEmKorpSI3PHd+i3JXBJN3xyA6MvJv7at23HffFHwA==",
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz",
|
||||
"integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.18.6",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.18.6",
|
||||
"@babel/helper-plugin-utils": "^7.18.9",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.18.9",
|
||||
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz",
|
||||
"integrity": "sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg=="
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz",
|
||||
"integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w=="
|
||||
},
|
||||
"@babel/helper-skip-transparent-expression-wrappers": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.6.tgz",
|
||||
"integrity": "sha512-4KoLhwGS9vGethZpAhYnMejWkX64wsnHPDwvOsKWU6Fg4+AlK2Jz3TyjQLMEPvz+1zemi/WBdkYxCD0bAfIkiw==",
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz",
|
||||
"integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.18.9"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
@@ -1827,9 +1834,9 @@
|
||||
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g=="
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.18.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.7.tgz",
|
||||
"integrity": "sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==",
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz",
|
||||
"integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
@@ -4382,6 +4389,49 @@
|
||||
"postcss": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||
"requires": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
|
||||
},
|
||||
"@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
|
||||
},
|
||||
"@jridgewell/source-map": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
|
||||
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@mdx-js/mdx": {
|
||||
"version": "1.6.22",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
|
||||
@@ -6446,6 +6496,11 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A=="
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -7148,20 +7203,14 @@
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
@@ -9405,6 +9454,11 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A=="
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -9947,20 +10001,14 @@
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
@@ -13086,13 +13134,13 @@
|
||||
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM="
|
||||
},
|
||||
"amplitude-js": {
|
||||
"version": "8.18.5",
|
||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.18.5.tgz",
|
||||
"integrity": "sha512-s43q4qKd7kvhYESQhYvyKDKUM1PpyAyoOFFlyMuFfQHRxyeDmZRhcfzrKnOhbrLhFxSWtPc0VEeh9tajJRNe5Q==",
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.0.tgz",
|
||||
"integrity": "sha512-kC01TmmCdDrtms8LhvC/r65FtbmCbNHZ1/jiezXmTH82TsWI/SkN47jKs8CCwjZNakqTBN/hmficiZBUKv4myw==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-connector": "1.4.4",
|
||||
"@amplitude/ua-parser-js": "0.7.31",
|
||||
"@amplitude/utils": "^1.0.5",
|
||||
"@amplitude/utils": "^1.10.1",
|
||||
"@babel/runtime": "^7.3.4",
|
||||
"blueimp-md5": "^2.10.0",
|
||||
"query-string": "5"
|
||||
@@ -15839,9 +15887,9 @@
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.23.5",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz",
|
||||
"integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg=="
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
||||
"integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.11.0",
|
||||
@@ -16293,7 +16341,7 @@
|
||||
"de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0="
|
||||
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
@@ -16771,9 +16819,9 @@
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
|
||||
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
|
||||
"integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.7.0",
|
||||
@@ -20627,9 +20675,9 @@
|
||||
}
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
|
||||
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz",
|
||||
"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw=="
|
||||
},
|
||||
"js-message": {
|
||||
"version": "1.0.5",
|
||||
@@ -27297,9 +27345,9 @@
|
||||
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="
|
||||
},
|
||||
"terser": {
|
||||
"version": "4.6.7",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.7.tgz",
|
||||
"integrity": "sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||
"requires": {
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
@@ -28265,9 +28313,60 @@
|
||||
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk="
|
||||
},
|
||||
"vue": {
|
||||
"version": "2.6.14",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
|
||||
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
|
||||
"version": "2.7.10",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.10.tgz",
|
||||
"integrity": "sha512-HmFC70qarSHPXcKtW8U8fgIkF6JGvjEmDiVInTkKZP0gIlEPhlVlcJJLkdGIDiNkIeA2zJPQTWJUI4iWe+AVfg==",
|
||||
"requires": {
|
||||
"@vue/compiler-sfc": "2.7.10",
|
||||
"csstype": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
|
||||
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg=="
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "2.7.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.10.tgz",
|
||||
"integrity": "sha512-55Shns6WPxlYsz4WX7q9ZJBL77sKE1ZAYNYStLs6GbhIOMrNtjMvzcob6gu3cGlfpCR4bT7NXgyJ3tly2+Hx8Q==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.18.4",
|
||||
"postcss": "^8.4.14",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
|
||||
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
|
||||
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-cli-plugin-storybook": {
|
||||
"version": "2.1.0",
|
||||
@@ -28584,12 +28683,12 @@
|
||||
}
|
||||
},
|
||||
"vue-template-compiler": {
|
||||
"version": "2.6.14",
|
||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz",
|
||||
"integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==",
|
||||
"version": "2.7.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz",
|
||||
"integrity": "sha512-QO+8R9YRq1Gudm8ZMdo/lImZLJVUIAM8c07Vp84ojdDAf8HmPJc7XB556PcXV218k2AkKznsRz6xB5uOjAC4EQ==",
|
||||
"requires": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.1.0"
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"vue-template-es2015-compiler": {
|
||||
|
||||
@@ -25,15 +25,15 @@
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.15",
|
||||
"@vue/cli-service": "^4.5.15",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.18.5",
|
||||
"amplitude-js": "^8.21.0",
|
||||
"axios": "^0.25.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"chai": "^4.3.6",
|
||||
"core-js": "^3.23.5",
|
||||
"dompurify": "^2.3.8",
|
||||
"core-js": "^3.24.1",
|
||||
"dompurify": "^2.4.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"hellojs": "^1.19.5",
|
||||
"inspectpack": "^4.7.1",
|
||||
"intro.js": "^5.1.0",
|
||||
"jquery": "^3.6.0",
|
||||
"jquery": "^3.6.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nconf": "^0.12.0",
|
||||
@@ -55,16 +55,16 @@
|
||||
"svgo-loader": "^2.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.7.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue": "^2.7.10",
|
||||
"vue-cli-plugin-storybook": "2.1.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vue-template-compiler": "^2.7.10",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^4.46.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.6"
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
execSync('npm run storybook:build', {
|
||||
/* execSync('npm run storybook:build', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}); */
|
||||
}
|
||||
|
||||
@@ -463,6 +463,11 @@
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
}
|
||||
.achievement-woodlandWizard2x {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-woodlandWizard2x.png');
|
||||
width: 60px;
|
||||
height: 64px;
|
||||
}
|
||||
.achievement-zodiac2x {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-zodiac2x.png');
|
||||
width: 60px;
|
||||
@@ -565,6 +570,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_autumn_picnic {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_picnic.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_autumn_poplars {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_poplars.png');
|
||||
width: 141px;
|
||||
@@ -690,6 +700,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_by_a_campfire {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_by_a_campfire.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_camping_out {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_camping_out.png');
|
||||
width: 141px;
|
||||
@@ -1354,6 +1369,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_messy_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_messy_room.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_meteor_shower {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_meteor_shower.png');
|
||||
width: 141px;
|
||||
@@ -1424,6 +1444,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_old_photo {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_old_photo.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_a_castle_wall {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_castle_wall.png');
|
||||
width: 141px;
|
||||
@@ -1509,6 +1534,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_rainbow_eucalyptus {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rainbow_eucalyptus.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_rainbow_meadow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rainbow_meadow.png');
|
||||
width: 141px;
|
||||
@@ -1819,6 +1849,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_theatre_stage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_theatre_stage.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_throne_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_throne_room.png');
|
||||
width: 141px;
|
||||
@@ -2106,6 +2141,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_autumn_picnic {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_autumn_picnic.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_autumn_poplars {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_autumn_poplars.png');
|
||||
width: 68px;
|
||||
@@ -2231,6 +2271,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_by_a_campfire {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_by_a_campfire.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_camping_out {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_camping_out.png');
|
||||
width: 68px;
|
||||
@@ -2900,6 +2945,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_messy_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_messy_room.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_meteor_shower {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_meteor_shower.png');
|
||||
width: 68px;
|
||||
@@ -2970,6 +3020,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_old_photo {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_old_photo.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_on_a_castle_wall {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_on_a_castle_wall.png');
|
||||
width: 68px;
|
||||
@@ -3055,6 +3110,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_rainbow_eucalyptus {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_rainbow_eucalyptus.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_rainbow_meadow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_rainbow_meadow.png');
|
||||
width: 68px;
|
||||
@@ -3370,6 +3430,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_theatre_stage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_theatre_stage.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_throne_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_throne_room.png');
|
||||
width: 68px;
|
||||
@@ -18475,6 +18540,11 @@
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.eyewear_armoire_comedyMask {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_armoire_comedyMask.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.eyewear_armoire_goofyGlasses {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_armoire_goofyGlasses.png');
|
||||
width: 90px;
|
||||
@@ -18485,6 +18555,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.eyewear_armoire_tragedyMask {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_armoire_tragedyMask.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.headAccessory_armoire_comicalArrow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_armoire_comicalArrow.png');
|
||||
width: 90px;
|
||||
@@ -18970,6 +19045,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_dustpan {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_dustpan.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_fancyBlownGlassVase {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_fancyBlownGlassVase.png');
|
||||
width: 114px;
|
||||
@@ -19685,6 +19765,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_eyewear_armoire_comedyMask {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_armoire_comedyMask.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_eyewear_armoire_goofyGlasses {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_armoire_goofyGlasses.png');
|
||||
width: 68px;
|
||||
@@ -19695,6 +19780,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_eyewear_armoire_tragedyMask {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_armoire_tragedyMask.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_headAccessory_armoire_comicalArrow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_armoire_comicalArrow.png');
|
||||
width: 68px;
|
||||
@@ -20180,6 +20270,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_armoire_dustpan {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_dustpan.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_armoire_fancyBlownGlassVase {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_fancyBlownGlassVase.png');
|
||||
width: 68px;
|
||||
@@ -20565,6 +20660,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_armoire_featherDuster {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_featherDuster.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_armoire_festivalFirecracker {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_festivalFirecracker.png');
|
||||
width: 68px;
|
||||
@@ -20805,6 +20905,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_armoire_pushBroom {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_pushBroom.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_armoire_rancherLasso {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_rancherLasso.png');
|
||||
width: 68px;
|
||||
@@ -21430,6 +21535,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_featherDuster {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_featherDuster.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_festivalFirecracker {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_festivalFirecracker.png');
|
||||
width: 90px;
|
||||
@@ -21670,6 +21780,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_pushBroom {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pushBroom.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_rancherLasso {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_rancherLasso.png');
|
||||
width: 90px;
|
||||
@@ -22760,6 +22875,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2022Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -22930,6 +23065,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2022Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -23055,6 +23210,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2022Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2022Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2022Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -23210,6 +23380,26 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_armor_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Healer.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_armor_special_fall2022Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Mage.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_armor_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Rogue.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_armor_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Warrior.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fallHealer.png');
|
||||
width: 68px;
|
||||
@@ -23380,6 +23570,26 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_head_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Healer.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_head_special_fall2022Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Mage.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_head_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Rogue.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_head_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Warrior.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_head_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fallHealer.png');
|
||||
width: 68px;
|
||||
@@ -23505,6 +23715,21 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2022Healer.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2022Rogue.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2022Warrior.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fallHealer.png');
|
||||
width: 68px;
|
||||
@@ -23660,6 +23885,26 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Healer.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_special_fall2022Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Mage.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Rogue.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Warrior.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fallHealer.png');
|
||||
width: 68px;
|
||||
@@ -23820,6 +24065,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2022Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -23980,6 +24245,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2022Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2022Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2022Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2022Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -26925,6 +27210,86 @@
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.eyewear_mystery_202208 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202208.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202208 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202208.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shop_eyewear_mystery_202208 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_mystery_202208.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_head_mystery_202208 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_mystery_202208.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_set_mystery_202208 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202208.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shield_mystery_202209 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202209.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shop_set_mystery_202209 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202209.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_mystery_202209 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_mystery_202209.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_mystery_202209 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_mystery_202209.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.weapon_mystery_202209 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202209.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202210 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202210.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.head_mystery_202210 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202210.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shop_armor_mystery_202210 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_mystery_202210.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_head_mystery_202210 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_mystery_202210.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_set_mystery_202210 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202210.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.slim_armor_mystery_202210 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202210.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.broad_armor_mystery_301404 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
|
||||
width: 90px;
|
||||
@@ -36603,6 +36968,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_BearCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_BearCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -37018,6 +37388,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Cactus-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Cactus-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -37533,6 +37908,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dragon-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dragon-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -37948,6 +38328,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_FlyingPig-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_FlyingPig-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -38213,6 +38598,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Fox-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Fox-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -38868,6 +39258,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_LionCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_LionCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -39353,6 +39748,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_PandaCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_PandaCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -40573,6 +40973,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_TigerCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_TigerCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -41148,6 +41553,11 @@
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Body_Wolf-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Porcelain.png');
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Body_Wolf-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Rainbow.png');
|
||||
width: 135px;
|
||||
@@ -41668,6 +42078,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_BearCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_BearCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -42083,6 +42498,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Cactus-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Cactus-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -42598,6 +43018,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dragon-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dragon-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -43013,6 +43438,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_FlyingPig-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_FlyingPig-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -43278,6 +43708,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Fox-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Fox-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -43933,6 +44368,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_LionCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_LionCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -44418,6 +44858,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_PandaCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_PandaCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -45638,6 +46083,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_TigerCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Porcelain.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_TigerCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Rainbow.png');
|
||||
width: 105px;
|
||||
@@ -46213,6 +46663,11 @@
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Head_Wolf-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Porcelain.png');
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Head_Wolf-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Rainbow.png');
|
||||
width: 135px;
|
||||
@@ -46738,6 +47193,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_BearCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_BearCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -47153,6 +47613,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Cactus-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Cactus-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -47668,6 +48133,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Dragon-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Dragon-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -48083,6 +48553,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_FlyingPig-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_FlyingPig-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -48348,6 +48823,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Fox-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Fox-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -49008,6 +49488,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_LionCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_LionCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -49493,6 +49978,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_PandaCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_PandaCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -50713,6 +51203,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_TigerCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_TigerCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -51288,6 +51783,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Wolf-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Mount_Icon_Wolf-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -51818,6 +52318,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -52248,6 +52753,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -52783,6 +53293,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -53213,6 +53728,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -53493,6 +54013,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -54168,6 +54693,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -54668,6 +55198,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -55913,6 +56448,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -56508,6 +57048,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Porcelain.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Rainbow.png');
|
||||
width: 81px;
|
||||
@@ -56838,6 +57383,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Porcelain {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Porcelain.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Purple {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Purple.png');
|
||||
width: 68px;
|
||||
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 446 B |
|
After Width: | Height: | Size: 741 B |
|
After Width: | Height: | Size: 979 B |
@@ -1,9 +1,56 @@
|
||||
.create-task-area {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
right: 12px;
|
||||
top: -24px;
|
||||
z-index: 998;
|
||||
align-items: center;
|
||||
|
||||
.dropdown {
|
||||
width: 140px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
|
||||
background-color: $white;
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
|
||||
.task-icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.task-label {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
.svg-icon {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
.svg-icon {
|
||||
color: $gray-200;
|
||||
|
||||
&.icon-habit {
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.icon-daily {
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.icon-todo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.icon-reward {
|
||||
width: 26px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide-tasks-btns-leave-active, .slide-tasks-btns-enter-active {
|
||||
@@ -17,70 +64,14 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.diamond-btn {
|
||||
margin-left: 24px;
|
||||
background: $white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
cursor: pointer;
|
||||
color: $gray-200;
|
||||
transform: rotate(45deg);
|
||||
|
||||
&:hover:not(.create-btn) {
|
||||
color: $purple-400;
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
&.icon-habit {
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.icon-daily {
|
||||
width: 21.6px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&.icon-todo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&.icon-reward {
|
||||
width: 23.4px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
color: $white;
|
||||
background-color: $green-100;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: $purple-200;
|
||||
height: 32px;
|
||||
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform: rotate(-45deg);
|
||||
transition: transform 0.3s cubic-bezier(0, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
&.open {
|
||||
background: $gray-200 !important;
|
||||
|
||||
.svg-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
color: $purple-500;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +180,8 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
}
|
||||
|
||||
// used in checkboxes and radios
|
||||
$bg-focused-active-control: #4f2993;
|
||||
$bg-disabled-control: #34303a;
|
||||
$bg-focused-active-control: $purple-200;
|
||||
$bg-disabled-control: $gray-10;
|
||||
|
||||
// custom control
|
||||
.custom-control {
|
||||
@@ -231,17 +231,21 @@ $bg-disabled-control: #34303a;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:focus:not(:checked):not(:disabled)~.custom-control-label::before, &:active:not(:checked):not(:disabled)~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
|
||||
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
|
||||
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
|
||||
border: 2px solid $gray-300;
|
||||
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
|
||||
}
|
||||
|
||||
&:focus:checked:not(:disabled)~.custom-control-label::before, &:active:checked:not(:disabled)~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
|
||||
border-color: $purple-400;
|
||||
&:focus:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:active:checked:not(:disabled)~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
|
||||
border-color: 2 px solid $purple-400;
|
||||
background-color: $purple-400;
|
||||
}
|
||||
|
||||
&:focus:disabled~.custom-control-label::before, &:active:disabled~.custom-control-label::before {
|
||||
&:focus:disabled~.custom-control-label::before,
|
||||
&:active:disabled~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
&-bg {
|
||||
background: $maroon-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $maroon-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $maroon-100 !important; }
|
||||
&-headings { color: $white; }
|
||||
&-headings { color: $white !important; }
|
||||
&-icon { color: $maroon-100 !important; }
|
||||
&-text { color: $red-1 !important; }
|
||||
&-content {
|
||||
@@ -61,7 +61,7 @@
|
||||
&-bg {
|
||||
background: $red-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $red-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -92,7 +92,7 @@
|
||||
&-bg {
|
||||
background: $orange-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $orange-100 !important; }
|
||||
&-inner-habit { background: rgba($orange-1, 0.25) !important; }
|
||||
@@ -123,7 +123,7 @@
|
||||
&-bg {
|
||||
background: $yellow-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $yellow-100 !important; }
|
||||
&-inner-habit { background: rgba($yellow-1, 0.25) !important; }
|
||||
@@ -154,7 +154,7 @@
|
||||
&-bg {
|
||||
background: $green-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $green-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -186,7 +186,7 @@
|
||||
&-bg {
|
||||
background: $teal-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $teal-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -217,7 +217,7 @@
|
||||
&-bg {
|
||||
background: $blue-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $blue-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -249,7 +249,7 @@
|
||||
&-bg {
|
||||
background: $purple-task !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
&-inner-daily-todo { background: rgba($white, 0.5) !important; }
|
||||
@@ -258,7 +258,7 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $purple-300 !important; }
|
||||
&-headings { color: $white; }
|
||||
&-headings { color: $white !important; }
|
||||
&-icon { color: $purple-300 !important; }
|
||||
&-text { color: $black !important; }
|
||||
&-content {
|
||||
@@ -281,11 +281,11 @@
|
||||
background: rgba($yellow-100, 0.15) !important;
|
||||
.small-text { color: $yellow-1 !important; }
|
||||
|
||||
&:hover { background: rgba($yellow-100, 0.25) !important; }
|
||||
&:hover:not(.task-not-scoreable) { background: rgba($yellow-100, 0.25) !important; }
|
||||
}
|
||||
&-bg-noninteractive {
|
||||
background: rgba($yellow-100, 0.15) !important;
|
||||
.small-text { color: $yellow-1 !important; }
|
||||
background-color: $gray-500 !important;
|
||||
.small-text { color: $gray-100 !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,7 +312,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $gray-200 !important;
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive {
|
||||
background: $gray-200 !important;
|
||||
@@ -323,7 +323,7 @@
|
||||
background: $gray-600;
|
||||
|
||||
.task-title, .task-notes {
|
||||
color: $gray-300 !important;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="16" viewBox="0 0 8 16">
|
||||
<path fill-rule="evenodd" d="M7.145 8.006H4.903V16H1.58V8.006H0V5.182h1.58V3.354C1.58 2.045 2.202 0 4.933 0l2.461.01v2.742H5.608c-.291 0-.705.145-.705.77v1.66h2.533l-.291 2.824z"/>
|
||||
<svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4C1.79,0,0,1.79,0,4V20c0,2.21,1.79,4,4,4H20c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4Zm-3.72,6.66h-1.26c-1.24,0-1.63,.77-1.63,1.56v1.88h2.78l-.44,2.9h-2.33v7h-3.13v-7h-2.54v-2.9h2.54v-2.21c0-2.51,1.5-3.9,3.78-3.9,1.1,0,2.24,.2,2.24,.2v2.47Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 354 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 0c2.173 0 2.445.01 3.298.048.852.04 1.433.174 1.942.372.526.205.973.478 1.418.922.444.445.717.892.922 1.418.198.509.333 1.09.372 1.942C15.99 5.555 16 5.827 16 8s-.01 2.445-.048 3.298c-.04.852-.174 1.433-.372 1.942a3.924 3.924 0 0 1-.922 1.418 3.924 3.924 0 0 1-1.418.922c-.509.198-1.09.333-1.942.372-.853.04-1.125.048-3.298.048s-2.445-.009-3.298-.048c-.852-.04-1.433-.174-1.942-.372a3.924 3.924 0 0 1-1.418-.922A3.924 3.924 0 0 1 .42 13.24c-.198-.509-.333-1.09-.372-1.942C.01 10.445 0 10.173 0 8s.01-2.445.048-3.298C.088 3.85.222 3.269.42 2.76c.205-.526.478-.973.922-1.418A3.924 3.924 0 0 1 2.76.42C3.269.222 3.85.087 4.702.048 5.555.01 5.827 0 8 0zm0 3.892a4.108 4.108 0 1 0 0 8.216 4.108 4.108 0 0 0 0-8.216zm5.23-.162a.96.96 0 1 0-1.92 0 .96.96 0 0 0 1.92 0zM8 10.667a2.666 2.666 0 1 1 0-5.333 2.666 2.666 0 0 1 0 5.333z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M14.67,12A2.67,2.67,0,1,1,12,9.33,2.67,2.67,0,0,1,14.67,12Zm3.89,0c0,1.16.1,3.66-.32,4.71a2.7,2.7,0,0,1-1.52,1.52c-1.06.42-3.55.33-4.72.33s-3.66.09-4.71-.33a2.65,2.65,0,0,1-1.52-1.52c-.42-1.05-.33-3.55-.33-4.71s-.09-3.67.33-4.72A2.65,2.65,0,0,1,7.29,5.76c1-.42,3.55-.32,4.71-.32a18,18,0,0,1,4.72.32,2.7,2.7,0,0,1,1.52,1.52C18.66,8.34,18.56,10.83,18.56,12ZM16.1,12A4.1,4.1,0,1,0,12,16.1,4.09,4.09,0,0,0,16.1,12Zm1.13-4.27a1,1,0,1,0-1,1A1,1,0,0,0,17.23,7.73ZM24,4V20a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V4A4,4,0,0,1,4,0H20A4,4,0,0,1,24,4ZM19.94,8.7a4.71,4.71,0,0,0-1.29-3.35A4.71,4.71,0,0,0,15.3,4.06C14,4,10,4,8.7,4.06A4.76,4.76,0,0,0,5.35,5.34,4.75,4.75,0,0,0,4.06,8.7C4,10,4,14,4.06,15.3a4.73,4.73,0,0,0,1.29,3.35A4.77,4.77,0,0,0,8.7,19.94c1.32.08,5.28.08,6.6,0a4.71,4.71,0,0,0,3.35-1.29,4.73,4.73,0,0,0,1.29-3.35C20,14,20,10,19.94,8.7Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 914 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m12.707 5.707-1.414-1.414L8 7.586 6.707 6.293 5.293 7.707 8 10.414l4.707-4.707zM16 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2h2v2h10V2H4v2.5h2l-3 3-3-3h2V2a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" fill="#878190" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM16.68,19a4.47,4.47,0,0,1-3,1C9.86,20,9,17.22,9,15.61v-4.5H7.56a.31.31,0,0,1-.31-.32V8.67a.52.52,0,0,1,.35-.5,4,4,0,0,0,2.63-3.66c0-.34.21-.51.51-.51H13a.31.31,0,0,1,.32.31v3.6h2.59a.31.31,0,0,1,.31.31v2.55a.31.31,0,0,1-.31.32H13.25v4.16c0,1.07.74,1.67,2.13,1.12a.67.67,0,0,1,.4-.07.36.36,0,0,1,.23.25l.68,2C16.75,18.71,16.8,18.89,16.68,19Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 516 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="13" viewBox="0 0 16 13">
|
||||
<path fill-rule="evenodd" d="M14.362 3.238c.007.141.01.281.01.424 0 4.338-3.302 9.34-9.34 9.34A9.284 9.284 0 0 1 0 11.527c.257.029.518.045.783.045a6.576 6.576 0 0 0 4.076-1.404 3.288 3.288 0 0 1-3.065-2.28 3.312 3.312 0 0 0 1.481-.056A3.288 3.288 0 0 1 .642 4.613v-.041c.444.246.949.393 1.488.41A3.28 3.28 0 0 1 .67 2.25c0-.602.162-1.166.444-1.651a9.315 9.315 0 0 0 6.766 3.43A3.28 3.28 0 0 1 11.078 0c.943 0 1.797.398 2.395 1.035a6.565 6.565 0 0 0 2.085-.797 3.289 3.289 0 0 1-1.443 1.816A6.543 6.543 0 0 0 16 1.539a6.665 6.665 0 0 1-1.638 1.699"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM18.36,8.74c0,.14,0,.29,0,.43A9.34,9.34,0,0,1,4,17a6.85,6.85,0,0,0,.79,0,6.57,6.57,0,0,0,4.07-1.4A3.29,3.29,0,0,1,5.8,13.39a4.1,4.1,0,0,0,.62,0,3.49,3.49,0,0,0,.86-.11,3.28,3.28,0,0,1-2.63-3.22v0a3.35,3.35,0,0,0,1.48.42A3.29,3.29,0,0,1,4.67,7.76,3.22,3.22,0,0,1,5.12,6.1a9.3,9.3,0,0,0,6.76,3.43,3.67,3.67,0,0,1-.08-.75,3.28,3.28,0,0,1,5.67-2.24,6.54,6.54,0,0,0,2.08-.79,3.22,3.22,0,0,1-1.44,1.8A6.67,6.67,0,0,0,20,7.05,7.31,7.31,0,0,1,18.36,8.74Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 622 B |
@@ -1,20 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<defs>
|
||||
<path id="rt3mruthma" d="M11.2 13.2c0-1.231-.467-2.35-1.23-3.2h1.23c1.765 0 3.2 1.435 3.2 3.2h-3.2zm-9.6 0c0-1.765 1.435-3.2 3.2-3.2h1.6c1.765 0 3.2 1.435 3.2 3.2h-8zm4-9.6C6.926 3.6 8 4.674 8 6c0 1.326-1.074 2.4-2.4 2.4-1.326 0-2.4-1.074-2.4-2.4 0-1.326 1.074-2.4 2.4-2.4zm3.452.415C9.436 3.754 9.9 3.6 10.4 3.6c1.326 0 2.4 1.074 2.4 2.4 0 1.326-1.074 2.4-2.4 2.4-.5 0-.964-.154-1.348-.415.34-.587.548-1.26.548-1.985 0-.726-.209-1.398-.548-1.985zm4.154 4.829C13.942 8.118 14.4 7.112 14.4 6c0-2.206-1.794-4-4-4-.904 0-1.73.313-2.4.82C7.33 2.314 6.504 2 5.6 2c-2.206 0-4 1.794-4 4 0 1.112.458 2.118 1.194 2.844C1.146 9.604 0 11.266 0 13.2c0 .884.716 1.6 1.6 1.6h12.8c.884 0 1.6-.716 1.6-1.6 0-1.934-1.146-3.596-2.794-4.356z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g>
|
||||
<g transform="translate(-1228 -1456) translate(1216 1416) translate(12 40)">
|
||||
<mask id="6szqyv7o1b" fill="#fff">
|
||||
<use xlink:href="#rt3mruthma"/>
|
||||
</mask>
|
||||
<use fill="#878190" xlink:href="#rt3mruthma"/>
|
||||
<g fill="#878190" mask="url(#6szqyv7o1b)">
|
||||
<path d="M0 0H16V16H0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12.8"><path d="M11.2,12.8A4.79,4.79,0,0,0,10,9.6H11.2a3.21,3.21,0,0,1,3.2,3.2Zm-9.6,0A3.21,3.21,0,0,1,4.8,9.6H6.4a3.21,3.21,0,0,1,3.2,3.2Zm4-9.6A2.4,2.4,0,1,1,3.2,5.6,2.39,2.39,0,0,1,5.6,3.2Zm3.45.42A2.35,2.35,0,0,1,10.4,3.2a2.4,2.4,0,1,1,0,4.8,2.35,2.35,0,0,1-1.35-.42,3.84,3.84,0,0,0,0-4Zm4.16,4.82A4,4,0,0,0,8,2.42,4,4,0,0,0,5.6,1.6,4,4,0,0,0,2.79,8.44,4.82,4.82,0,0,0,0,12.8a1.6,1.6,0,0,0,1.6,1.6H14.4A1.6,1.6,0,0,0,16,12.8a4.82,4.82,0,0,0-2.79-4.36Z" transform="translate(0 -1.6)" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 569 B |
@@ -22,11 +22,6 @@
|
||||
Account created:
|
||||
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Most recent cron:
|
||||
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
("auth.timestamps.loggedin")
|
||||
</div>
|
||||
<div v-if="cronError">
|
||||
"lastCron" value:
|
||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||
@@ -36,6 +31,19 @@
|
||||
("auth.timestamps.loggedin" and "lastCron" dates are different).
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<div>
|
||||
Most recent cron:
|
||||
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
("auth.timestamps.loggedin")
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary ml-2"
|
||||
@click="resetCron()"
|
||||
>
|
||||
Reset Cron to Yesterday
|
||||
</button>
|
||||
</div>
|
||||
<div class="subsection-start">
|
||||
Time zone:
|
||||
<strong>{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
|
||||
@@ -218,6 +226,10 @@ export default {
|
||||
await this.saveHero({ hero: this.hero, msg: 'API Token' });
|
||||
this.tokenModified = true;
|
||||
},
|
||||
resetCron () {
|
||||
this.hero.resetCron = true;
|
||||
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<subscription-and-perks
|
||||
:hero="hero"
|
||||
/>
|
||||
|
||||
<cron-and-auth
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
@@ -97,6 +101,7 @@ import AvatarAndDrops from './avatarAndDrops';
|
||||
import PrivilegesAndGems from './privilegesAndGems';
|
||||
import ContributorDetails from './contributorDetails';
|
||||
import Transactions from './transactions';
|
||||
import SubscriptionAndPerks from './subscriptionAndPerks';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
@@ -110,6 +115,7 @@ export default {
|
||||
PrivilegesAndGems,
|
||||
ContributorDetails,
|
||||
Transactions,
|
||||
SubscriptionAndPerks,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
||||
<div v-if="hero.purchased.plan.paymentMethod">
|
||||
Payment method:
|
||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.planId">
|
||||
Payment schedule ("basic-earned" is monthly):
|
||||
<strong>{{ hero.purchased.plan.planId }}</strong>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.dateCreated">
|
||||
Creation date:
|
||||
<strong>{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Termination date:
|
||||
<strong
|
||||
v-if="hero.purchased.plan.dateTerminated"
|
||||
>
|
||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</strong>
|
||||
<strong v-else> None </strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Consecutive months:
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.count"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Months until renewal:
|
||||
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Next Mystic Hourglass:
|
||||
<strong>{{ nextHourglassDate }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Mystic Hourglasses:
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.trinkets"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Gem cap:
|
||||
<strong>{{ hero.purchased.plan.consecutive.gemCapExtra + 25 }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Gems bought this month:
|
||||
<input
|
||||
v-model="hero.purchased.plan.gemsBought"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.purchased.plan.extraMonths > 0"
|
||||
>
|
||||
Additional credit (applied upon cancellation):
|
||||
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Mystery Items:
|
||||
<span
|
||||
v-if="hero.purchased.plan.mysteryItems.length > 0"
|
||||
>
|
||||
<span
|
||||
v-for="(item, index) in hero.purchased.plan.mysteryItems"
|
||||
:key="index"
|
||||
>
|
||||
<strong v-if="index < hero.purchased.plan.mysteryItems.length - 1">
|
||||
{{ item }},
|
||||
</strong>
|
||||
<strong v-else> {{ item }} </strong>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<strong>None</strong>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import { getPlanContext } from '@/../../common/script/cron';
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
nextHourglassDate () {
|
||||
const currentPlanContext = getPlanContext(this.hero, new Date());
|
||||
|
||||
return currentPlanContext.nextHourglassDate.format('MMMM');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'hero.purchased.plan.consecutive.count' () { // eslint-disable-line object-shorthand
|
||||
this.hero.purchased.plan.consecutive.gemCapExtra = Math.min(
|
||||
Math.floor(this.hero.purchased.plan.consecutive.count / 3) * 5, 25,
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
return moment(date).format('YYYY/MM/DD');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,21 +1,5 @@
|
||||
<template>
|
||||
<div class="form">
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('facebook')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.facebookIcon"
|
||||
></div>
|
||||
<span>{{ registering
|
||||
? $t('signUpWithSocial', {social: 'Facebook'})
|
||||
: $t('loginWithSocial', {social: 'Facebook'}) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12">
|
||||
<div
|
||||
@@ -243,7 +227,6 @@ import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
|
||||
@@ -260,7 +243,6 @@ export default {
|
||||
};
|
||||
|
||||
data.icons = Object.freeze({
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
});
|
||||
@@ -308,8 +290,6 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
|
||||
@@ -621,7 +621,6 @@ import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import exclamation from '@/assets/svg/exclamation.svg';
|
||||
import gryphon from '@/assets/svg/gryphon.svg';
|
||||
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
|
||||
@@ -644,7 +643,6 @@ export default {
|
||||
exclamation,
|
||||
gryphon,
|
||||
habiticaIcon,
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
});
|
||||
@@ -734,8 +732,6 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
@@ -757,8 +753,7 @@ export default {
|
||||
}, 500),
|
||||
sanitizeRedirect (redirect) {
|
||||
if (!redirect) return '/';
|
||||
let sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
sanitizedString = `/${sanitizedString}`;
|
||||
const sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
return sanitizedString;
|
||||
},
|
||||
async register () {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<customize-options
|
||||
:items="set.options"
|
||||
:current-value="user.preferences.hair.color"
|
||||
:full-set="!hideSet(set) && !userOwnsSet('hair', set.keys, 'color')"
|
||||
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
|
||||
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<customize-options
|
||||
:items="set.options"
|
||||
:current-value="user.preferences.skin"
|
||||
:full-set="!hideSet(set) && !userOwnsSet('skin', set.keys)"
|
||||
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
|
||||
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -112,10 +112,8 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'group._id': {
|
||||
async groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
'group._id': function groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- THIS IS A VERY OLD FILE DO NOT USE -->
|
||||
<template>
|
||||
<div class="create-group-modal-pages">
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="create-group"
|
||||
:title="activePage === PAGES.CREATE_GROUP ? 'Create your Group' : 'Select Payment'"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
size="md"
|
||||
@hide="onHide()"
|
||||
>
|
||||
<div
|
||||
v-if="activePage === PAGES.CREATE_GROUP"
|
||||
class="col-12"
|
||||
>
|
||||
<!-- HEADER -->
|
||||
<div
|
||||
class="modal-close"
|
||||
>
|
||||
<span
|
||||
class="cancel-text"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-primary next-button"
|
||||
:value="$t('next')"
|
||||
:disabled="!newGroupIsReady"
|
||||
@click="createGroup()"
|
||||
>
|
||||
{{ $t('next') }}
|
||||
</button>
|
||||
</div>
|
||||
<h2>{{ $t('createGroup') }}</h2>
|
||||
|
||||
<!-- FORM -->
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
:text="$t('nameStar')"
|
||||
/>
|
||||
<input
|
||||
id="new-group-name"
|
||||
v-model="newGroup.name"
|
||||
class="form-control input-medium option-content name-input"
|
||||
required="required"
|
||||
type="text"
|
||||
:placeholder="$t('nameStarText')"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
for="new-group-description"
|
||||
:text="$t('descriptionOptional')"
|
||||
class="description-label"
|
||||
/>
|
||||
<div class="characters-remaining">
|
||||
{{ $t('charactersRemaining', {characters: charactersRemaining}) }}
|
||||
</div>
|
||||
<textarea
|
||||
id="new-group-description"
|
||||
v-model="newGroup.description"
|
||||
class="form-control option-content description-input"
|
||||
cols="3"
|
||||
:placeholder="$t('descriptionOptionalText')"
|
||||
maxlength="250"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="create-group-leaderOnlyChallenges-checkbox"
|
||||
v-model="newGroup.leaderOnly.challenges"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="custom-control-label"
|
||||
for="create-group-leaderOnlyChallenges-checkbox"
|
||||
>{{ $t('leaderOnlyChallenges') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
:text="$t('groupUse')"
|
||||
/>
|
||||
<select-translated-array
|
||||
:items="[
|
||||
'groupParentChildren',
|
||||
'groupCouple',
|
||||
'groupFriends',
|
||||
'groupCoworkers',
|
||||
'groupManager',
|
||||
'groupTeacher'
|
||||
]"
|
||||
class="group-input"
|
||||
:placeholder="'groupUseDefault'"
|
||||
:value="newGroup.demographics"
|
||||
@select="newGroup.demographics = $event"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-primary btn-lg btn-block btn-payment"
|
||||
:disabled="!newGroupIsReady"
|
||||
@click="createGroup()"
|
||||
>
|
||||
{{ $t('nextPaymentMethod') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PAYMENT -->
|
||||
<!-- @TODO: Separate payment into a separate modal -->
|
||||
<div
|
||||
v-if="activePage === PAGES.PAY"
|
||||
class="col-12 payments"
|
||||
>
|
||||
<div class="text-center">
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
h2 {
|
||||
color: $purple-300;
|
||||
font-size: 1.25rem;
|
||||
height: 28px;
|
||||
width: 120px;
|
||||
margin-top: 24px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.cancel-text {
|
||||
color: $blue-10;
|
||||
font-size: 0.875rem;
|
||||
margin-right: 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.next-button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: $gray-50;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.description-label {
|
||||
margin-bottom: -24px;
|
||||
}
|
||||
|
||||
.name-input, .description-input, .group-input {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.characters-remaining {
|
||||
color: $gray-100;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
text-align: right;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.description-input {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin-bottom: 16px !important;
|
||||
}
|
||||
|
||||
.btn-payment {
|
||||
margin: 24px 112px 24px 112px;
|
||||
width: 177px;
|
||||
}
|
||||
|
||||
.payments {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.payment-options {
|
||||
margin-bottom: 4em;
|
||||
|
||||
.purple-box {
|
||||
background-color: #4f2a93;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
border-radius: 8px;
|
||||
width: 200px;
|
||||
height: 215px;
|
||||
|
||||
.dollar {
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100px;
|
||||
margin-left: .3em;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.box, .purple-box {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
#create-group {
|
||||
.modal-dialog {
|
||||
max-width: 448px;
|
||||
}
|
||||
.modal-content {
|
||||
width: 448px;
|
||||
max-height: 436px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
|
||||
}
|
||||
.modal-body{
|
||||
padding: 0px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
cursor: pointer;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import { mapState } from '@/libs/store';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
paymentsButtons,
|
||||
selectTranslatedArray,
|
||||
lockableLabel,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
PAGES: {
|
||||
CREATE_GROUP: 'create-group',
|
||||
// UPGRADE_GROUP: 'upgrade-group',
|
||||
PAY: 'pay',
|
||||
},
|
||||
PAYMENTS: {
|
||||
AMAZON: 'amazon',
|
||||
STRIPE: 'stripe',
|
||||
},
|
||||
paymentMethod: '',
|
||||
newGroup: {
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
name: '',
|
||||
description: '',
|
||||
leaderOnly: {
|
||||
challenges: false,
|
||||
},
|
||||
demographics: null,
|
||||
user: '',
|
||||
},
|
||||
activePage: 'create-group',
|
||||
type: 'guild',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
newGroupIsReady () {
|
||||
return Boolean(this.newGroup.name) && Boolean(this.newGroup.demographics);
|
||||
},
|
||||
charactersRemaining () {
|
||||
const currentLength = this.newGroup.description ? this.newGroup.description.length : 0;
|
||||
return 250 - currentLength;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'create-group');
|
||||
},
|
||||
changePage (page) {
|
||||
this.activePage = page;
|
||||
},
|
||||
createGroup () {
|
||||
this.changePage(this.PAGES.PAY);
|
||||
},
|
||||
pay (paymentMethod) {
|
||||
const subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
|
||||
const demographicsKey = this.newGroup.demographics;
|
||||
const paymentData = {
|
||||
subscription: subscriptionKey,
|
||||
coupon: null,
|
||||
demographics: demographicsKey,
|
||||
};
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan create',
|
||||
eventAction: 'group plan create',
|
||||
eventCategory: 'behavior',
|
||||
demographics: this.newGroup.demographics,
|
||||
type: this.newGroup.type,
|
||||
}, { trackOnClient: true });
|
||||
|
||||
if (this.upgradingGroup && this.upgradingGroup._id) {
|
||||
paymentData.groupId = this.upgradingGroup._id;
|
||||
paymentData.group = this.upgradingGroup;
|
||||
} else {
|
||||
paymentData.groupToCreate = this.newGroup;
|
||||
}
|
||||
|
||||
this.paymentMethod = paymentMethod;
|
||||
|
||||
if (this.paymentMethod === this.PAYMENTS.AMAZON) {
|
||||
paymentData.type = 'subscription';
|
||||
return paymentData;
|
||||
}
|
||||
|
||||
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
|
||||
this.redirectToStripe(paymentData);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
onHide () {
|
||||
this.sendingInProgress = false;
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="group-plans-update"
|
||||
title="New Shared Task Board"
|
||||
size="lg"
|
||||
hide-footer="hide-footer"
|
||||
:no-close-on-backdrop="true"
|
||||
:no-close-on-esc="true"
|
||||
:centered="true"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
class="w-100 d-flex pt-2 justify-content-center"
|
||||
>
|
||||
<h2
|
||||
class="mx-auto mt-4"
|
||||
v-once
|
||||
>
|
||||
{{ $t('newGroupsWelcome') }}
|
||||
</h2>
|
||||
<div
|
||||
class="svg-icon color close-x ml-auto my-auto"
|
||||
aria-hidden="true"
|
||||
tabindex="0"
|
||||
@click="close()"
|
||||
@keypress.enter="close()"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
<img
|
||||
class="task-columns"
|
||||
src="~@/assets/images/group-plans/task-columns.png"
|
||||
srcset="~@/assets/images/group-plans/task-columns@2x.png 2x,
|
||||
~@/assets/images/group-plans/task-columns@3x.png 3x"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center p-4"
|
||||
>
|
||||
<div class="d-flex align-items-center justify-content-center mb-3">
|
||||
<div
|
||||
class="svg-icon sparkles my-auto mr-3"
|
||||
v-html="icons.sparkles"
|
||||
>
|
||||
</div>
|
||||
<h3
|
||||
class="my-auto"
|
||||
v-once
|
||||
>
|
||||
{{ $t('newGroupsWhatsNew') }}
|
||||
</h3>
|
||||
<div
|
||||
class="svg-icon sparkles my-auto ml-3 flip-x"
|
||||
v-html="icons.sparkles"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="mb-4 px-4"
|
||||
>
|
||||
<li>{{ $t('newGroupsBullet01') }}</li>
|
||||
<li>{{ $t('newGroupsBullet02') }}</li>
|
||||
<li>{{ $t('newGroupsBullet03') }}</li>
|
||||
<li>{{ $t('newGroupsBullet04') }}</li>
|
||||
<li>{{ $t('newGroupsBullet05') }}</li>
|
||||
<li>{{ $t('newGroupsBullet06') }}</li>
|
||||
<li>{{ $t('newGroupsBullet07') }}</li>
|
||||
<li>{{ $t('newGroupsBullet08') }}</li>
|
||||
<li>{{ $t('newGroupsBullet09') }}</li>
|
||||
<li>{{ $t('newGroupsBullet10') }}
|
||||
<ul class="p-0">
|
||||
<li v-html="$t('newGroupsBullet10a')"></li>
|
||||
<li v-html="$t('newGroupsBullet10b')"></li>
|
||||
<li v-html="$t('newGroupsBullet10c')"></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="mx-auto"
|
||||
v-html="$t('newGroupsVisitFAQ')"
|
||||
></div>
|
||||
<div
|
||||
class="mx-auto"
|
||||
>
|
||||
{{ $t('newGroupsEnjoy') }}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary mt-4 mb-1 mx-auto"
|
||||
@click="close()"
|
||||
@keypress.enter="close()"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
#group-plans-update {
|
||||
.modal-content {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
max-width: 566px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-image: linear-gradient(64deg, $purple-200, $purple-300 55%);
|
||||
height: 136px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-header, .modal-body, .modal-footer {
|
||||
padding: 0px;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
h2 {
|
||||
color: $white;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
line-height: 24px;
|
||||
list-style-type: square;
|
||||
li ul {
|
||||
list-style-type: none;
|
||||
li::before {
|
||||
margin-right: 1rem;
|
||||
font-size: 16px;
|
||||
content: '▫';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.close-x {
|
||||
color: $purple-400;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
right: 18px;
|
||||
top: 18px;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
width: 32px;
|
||||
height: 17px;
|
||||
|
||||
&.flip-x {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.task-columns {
|
||||
position: absolute;
|
||||
top: 84px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
sparkles,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$store.dispatch('user:set', { 'flags.tour.groupPlans': -2 });
|
||||
this.$root.$emit('bv::hide::modal', 'group-plans-update');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -28,7 +28,7 @@
|
||||
{{ $t('groupBilling') }}
|
||||
</router-link>
|
||||
</secondary-menu>
|
||||
<div class="col-12">
|
||||
<div class="col-12 px-0">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="standard-page">
|
||||
<div
|
||||
class="standard-page"
|
||||
@click="openCreateBtn ? openCreateBtn = false : null"
|
||||
>
|
||||
<group-plan-overview-modal />
|
||||
<task-modal
|
||||
ref="taskModal"
|
||||
@@ -7,65 +10,80 @@
|
||||
:purpose="taskFormPurpose"
|
||||
:group-id="groupId"
|
||||
@cancel="cancelTaskModal()"
|
||||
@taskCreated="taskCreated"
|
||||
@taskEdited="taskEdited"
|
||||
@taskCreated="loadTasks"
|
||||
@taskEdited="loadTasks"
|
||||
@taskDestroyed="taskDestroyed"
|
||||
/>
|
||||
<div class="row tasks-navigation">
|
||||
<div class="col-12 col-md-4">
|
||||
<h1>{{ $t('groupTasksTitle') }}</h1>
|
||||
<task-summary
|
||||
ref="taskSummary"
|
||||
:task="editingTask"
|
||||
@cancel="cancelTaskModal()"
|
||||
/>
|
||||
<div class="d-flex flex-wrap align-items-center mb-4">
|
||||
<div class="mr-auto">
|
||||
<h1>{{ group.name }}</h1>
|
||||
</div>
|
||||
<!-- @TODO: Abstract to component!-->
|
||||
<div class="col-12 col-md-4">
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="form-control input-search"
|
||||
type="text"
|
||||
:placeholder="$t('search')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="canCreateTasks"
|
||||
class="create-task-area d-flex"
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="form-control input-search"
|
||||
type="text"
|
||||
:placeholder="$t('search')"
|
||||
>
|
||||
<transition name="slide-tasks-btns">
|
||||
<div
|
||||
class="d-flex flex-wrap align-items-center justify-content-end ml-auto"
|
||||
>
|
||||
<toggle-switch
|
||||
id="taskMirrorToggle"
|
||||
class="mr-3 mb-1 ml-auto"
|
||||
:label="'Copy tasks'"
|
||||
:checked="user.preferences.tasks.mirrorGroupTasks.indexOf(group._id) !== -1"
|
||||
:hover-text="'Show assigned and open tasks on your personal task board'"
|
||||
@change="changeMirrorPreference"
|
||||
/>
|
||||
<div
|
||||
class="day-start d-flex align-items-center"
|
||||
v-html="$t('dayStart', { startTime: groupStartTime } )"
|
||||
>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<button
|
||||
id="create-task-btn"
|
||||
v-if="canCreateTasks"
|
||||
class="btn btn-primary create-btn d-flex align-items-center"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click.stop.prevent="openCreateBtn = !openCreateBtn"
|
||||
@keypress.enter="openCreateBtn = !openCreateBtn"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="svg-icon icon-10 color"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
|
||||
</button>
|
||||
<div
|
||||
v-if="openCreateBtn"
|
||||
class="d-flex"
|
||||
class="dropdown"
|
||||
>
|
||||
<div
|
||||
v-for="type in columns"
|
||||
:key="type"
|
||||
v-b-tooltip.hover.bottom="$t(type)"
|
||||
class="create-task-btn diamond-btn"
|
||||
@click="createTask(type)"
|
||||
class="dropdown-item d-flex px-2 py-1"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:class="`icon-${type}`"
|
||||
v-html="icons[type]"
|
||||
></div>
|
||||
<div class="d-flex align-items-center justify-content-center task-icon">
|
||||
<div
|
||||
class="svg-icon m-auto"
|
||||
:class="`icon-${type}`"
|
||||
v-html="icons[type]"
|
||||
></div>
|
||||
</div>
|
||||
<div class="task-label ml-2">
|
||||
{{ $t(type) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div
|
||||
id="create-task-btn"
|
||||
class="create-btn diamond-btn btn btn-success"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click="openCreateBtn = !openCreateBtn"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
</div>
|
||||
<b-tooltip
|
||||
v-if="!openCreateBtn"
|
||||
target="create-task-btn"
|
||||
placement="bottom"
|
||||
>
|
||||
{{ $t('create') }}
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -79,6 +97,7 @@
|
||||
:search-text="searchText"
|
||||
:draggable-override="canCreateTasks"
|
||||
@editTask="editTask"
|
||||
@taskSummary="taskSummary"
|
||||
@loadGroupCompletedTodos="loadGroupCompletedTodos"
|
||||
@taskDestroyed="taskDestroyed"
|
||||
/>
|
||||
@@ -86,12 +105,58 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#taskMirrorToggle {
|
||||
font-weight: bold;
|
||||
|
||||
.svg-icon {
|
||||
margin: 3px 6px 0px 4px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.toggle-switch-description {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/create-task.scss';
|
||||
|
||||
.tasks-navigation {
|
||||
margin-bottom: 40px;
|
||||
h1 {
|
||||
color: $purple-300;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.day-start {
|
||||
height: 2rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 2px;
|
||||
color: $gray-100;
|
||||
background-color: $gray-600;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
.input-search {
|
||||
margin-left: 12.5rem;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.input-search {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
#create-task-btn {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.positive {
|
||||
@@ -107,11 +172,15 @@
|
||||
import Vue from 'vue';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import moment from 'moment';
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import TaskColumn from '../tasks/column';
|
||||
import TaskModal from '../tasks/taskModal';
|
||||
import TaskSummary from '../tasks/taskSummary';
|
||||
import GroupPlanOverviewModal from './groupPlanOverviewModal';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
|
||||
import sync from '../../mixins/sync';
|
||||
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import filterIcon from '@/assets/svg/filter.svg';
|
||||
@@ -121,14 +190,18 @@ import dailyIcon from '@/assets/svg/daily.svg';
|
||||
import todoIcon from '@/assets/svg/todo.svg';
|
||||
import rewardIcon from '@/assets/svg/reward.svg';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TaskColumn,
|
||||
TaskModal,
|
||||
TaskSummary,
|
||||
GroupPlanOverviewModal,
|
||||
toggleSwitch,
|
||||
},
|
||||
mixins: [sync],
|
||||
props: ['groupId'],
|
||||
data () {
|
||||
return {
|
||||
@@ -199,6 +272,16 @@ export default {
|
||||
return (this.group.leader && this.group.leader._id === this.user._id)
|
||||
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
|
||||
},
|
||||
groupStartTime () {
|
||||
if (!this.group || !this.group.cron) return null;
|
||||
const { dayStart, timezoneOffset } = this.group.cron;
|
||||
const timezoneDiff = this.user.preferences.timezoneOffset - timezoneOffset;
|
||||
return moment()
|
||||
.hour(dayStart)
|
||||
.minute(0)
|
||||
.subtract(timezoneDiff, 'minutes')
|
||||
.format('h:mm A');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes (when this route is already active)
|
||||
@@ -208,6 +291,10 @@ export default {
|
||||
this.$set(this, 'searchId', to.params.groupId);
|
||||
next();
|
||||
},
|
||||
async beforeRouteLeave (to, from, next) {
|
||||
await this.sync();
|
||||
next();
|
||||
},
|
||||
mounted () {
|
||||
if (!this.searchId) this.searchId = this.groupId;
|
||||
this.load();
|
||||
@@ -218,6 +305,7 @@ export default {
|
||||
|
||||
this.$root.$on('habitica:team-sync', () => {
|
||||
this.loadTasks();
|
||||
this.loadGroupCompletedTodos();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
@@ -238,6 +326,9 @@ export default {
|
||||
this.group.members = members;
|
||||
|
||||
this.loadTasks();
|
||||
if (this.user.flags.tour.groupPlans !== -2) {
|
||||
this.$root.$emit('bv::show::modal', 'group-plans-update');
|
||||
}
|
||||
},
|
||||
async loadTasks () {
|
||||
this.tasksByType = {
|
||||
@@ -251,22 +342,13 @@ export default {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
const groupedApprovals = await this.loadApprovals();
|
||||
|
||||
tasks.forEach(task => {
|
||||
if (
|
||||
groupedApprovals[task._id]
|
||||
&& groupedApprovals[task._id].length > 0
|
||||
) task.approvals = groupedApprovals[task._id];
|
||||
this.tasksByType[task.type].push(task);
|
||||
});
|
||||
},
|
||||
async loadApprovals () {
|
||||
const approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
return groupBy(approvalRequests, 'group.taskId');
|
||||
if (this.editingTask && this.editingTask.completed) {
|
||||
this.loadGroupCompletedTodos();
|
||||
}
|
||||
},
|
||||
editTask (task) {
|
||||
this.taskFormPurpose = 'edit';
|
||||
@@ -277,6 +359,12 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
taskSummary (task) {
|
||||
this.editingTask = cloneDeep(task);
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-summary');
|
||||
});
|
||||
},
|
||||
async loadGroupCompletedTodos () {
|
||||
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
|
||||
groupId: this.searchId,
|
||||
@@ -299,14 +387,6 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
taskCreated (task) {
|
||||
task.group.id = this.group._id;
|
||||
this.tasksByType[task.type].unshift(task);
|
||||
},
|
||||
taskEdited (task) {
|
||||
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
|
||||
this.tasksByType[task.type].splice(index, 1, task);
|
||||
},
|
||||
taskDestroyed (task) {
|
||||
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
|
||||
this.tasksByType[task.type].splice(index, 1);
|
||||
@@ -354,6 +434,25 @@ export default {
|
||||
if (this.temporarilySelectedTags.indexOf(tagId) !== -1) return true;
|
||||
return false;
|
||||
},
|
||||
changeMirrorPreference (newVal) {
|
||||
Analytics.track({
|
||||
eventName: 'mirror tasks',
|
||||
eventAction: 'mirror tasks',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
mirror: newVal,
|
||||
group: this.group._id,
|
||||
}, { trackOnClient: true });
|
||||
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
|
||||
if (newVal) { // we're turning copy ON for this group
|
||||
groupsToMirror.push(this.group._id);
|
||||
} else { // we're turning copy OFF for this group
|
||||
groupsToMirror.splice(groupsToMirror.indexOf(this.group._id), 1);
|
||||
}
|
||||
this.$store.dispatch('user:set', {
|
||||
'preferences.tasks.mirrorGroupTasks': groupsToMirror,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<!-- @TODO: Move to group plans folder-->
|
||||
<div>
|
||||
<group-plan-creation-modal />
|
||||
<div>
|
||||
<div class="header">
|
||||
<h1 class="text-center">
|
||||
@@ -51,6 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upgrading an existing group -->
|
||||
<div
|
||||
v-if="upgradingGroup._id"
|
||||
id="upgrading-group"
|
||||
@@ -92,7 +94,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="box payment-providers">
|
||||
<h3>Choose your payment method</h3>
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
@@ -101,154 +102,53 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Create a new group -->
|
||||
<div
|
||||
v-if="!upgradingGroup._id"
|
||||
class="container col-6 offset-3 create-option"
|
||||
>
|
||||
<div class="row">
|
||||
<h1 class="col-12 text-center purple-header">
|
||||
Create your Group today!
|
||||
Create Your Group Today!
|
||||
</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary create-group"
|
||||
@click="launchModal('create')"
|
||||
@click="launchModal('create-page')"
|
||||
>
|
||||
Create Your New Group
|
||||
Create Your New Group!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pricing">
|
||||
<div class="col-5">
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="number">
|
||||
9
|
||||
</div>
|
||||
<div class="name">
|
||||
<div>Group Owner</div>
|
||||
<div>Subscription</div>
|
||||
</div>
|
||||
<div class="row pricing justify-content-center align-items-center">
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<div class="plus">
|
||||
+
|
||||
</div>
|
||||
<div class="number">
|
||||
9
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="number">
|
||||
3
|
||||
</div>
|
||||
<div class="name">
|
||||
<div>Each Additional</div>
|
||||
<div>Member</div>
|
||||
</div>
|
||||
<div class="name">
|
||||
<div>Group Owner</div>
|
||||
<div>Subscription</div>
|
||||
</div>
|
||||
<div class="plus">
|
||||
+
|
||||
</div>
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="number">
|
||||
3
|
||||
</div>
|
||||
<div class="name">
|
||||
<div>Each Additional</div>
|
||||
<div>Member</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal
|
||||
id="group-plan-modal"
|
||||
:title="activePage === PAGES.CREATE_GROUP ? 'Create your Group' : 'Select Payment'"
|
||||
size="md"
|
||||
hide-footer="hide-footer"
|
||||
>
|
||||
<div
|
||||
v-if="activePage === PAGES.CREATE_GROUP"
|
||||
class="col-12"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label
|
||||
class="control-label"
|
||||
for="new-group-name"
|
||||
>Name</label>
|
||||
<input
|
||||
id="new-group-name"
|
||||
v-model="newGroup.name"
|
||||
class="form-control input-medium option-content"
|
||||
required="required"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-group-description">{{ $t('description') }}</label>
|
||||
<textarea
|
||||
id="new-group-description"
|
||||
v-model="newGroup.description"
|
||||
class="form-control option-content"
|
||||
cols="3"
|
||||
:placeholder="$t('description')"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="type === 'guild'"
|
||||
class="form-group"
|
||||
>
|
||||
<div class="custom-control custom-radio">
|
||||
<input
|
||||
v-model="newGroup.privacy"
|
||||
class="custom-control-input"
|
||||
type="radio"
|
||||
name="new-group-privacy"
|
||||
value="private"
|
||||
>
|
||||
<label class="custom-control-label">{{ $t('inviteOnly') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="create-group-leaderOnlyChallenges-checkbox"
|
||||
v-model="newGroup.leaderOnly.challenges"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="custom-control-label"
|
||||
for="create-group-leaderOnlyChallenges-checkbox"
|
||||
>{{ $t('leaderOnlyChallenges') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="type === 'party'"
|
||||
class="form-group"
|
||||
>
|
||||
<button
|
||||
class="btn btn-secondary form-control"
|
||||
:value="$t('create')"
|
||||
@click="createGroup()"
|
||||
></button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-primary btn-lg btn-block"
|
||||
:disabled="!newGroupIsReady"
|
||||
@click="createGroup()"
|
||||
>
|
||||
{{ $t('create') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="activePage === PAGES.PAY"
|
||||
class="col-12"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3>Choose your payment method</h3>
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -260,12 +160,17 @@
|
||||
|
||||
.dollar {
|
||||
position: absolute;
|
||||
left: -1em;
|
||||
top: 1em;
|
||||
left: -16px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.purple-box {
|
||||
color: #bda8ff;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
}
|
||||
|
||||
.number {
|
||||
@@ -279,6 +184,8 @@
|
||||
|
||||
.payment-providers {
|
||||
width: 350px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,9 +193,9 @@
|
||||
background: #432874;
|
||||
background: linear-gradient(180deg, #4F2A93 0%, #432874 100%);
|
||||
color: #fff;
|
||||
padding: 2em;
|
||||
padding: 32px;
|
||||
height: 340px;
|
||||
margin-bottom: 2em;
|
||||
margin-bottom: 32px;
|
||||
margin-left: -12px;
|
||||
margin-right: -12px;
|
||||
|
||||
@@ -312,6 +219,7 @@
|
||||
|
||||
.box {
|
||||
height: 416px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@@ -354,17 +262,19 @@
|
||||
button.create-group {
|
||||
width: 330px;
|
||||
height: 96px;
|
||||
border-radius: 8px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.purple-header {
|
||||
color: #6133b4;
|
||||
font-size: 48px;
|
||||
margin-top: 1em;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.pricing {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 4em;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 64px;
|
||||
|
||||
.dollar, .number, .name {
|
||||
display: inline-block;
|
||||
@@ -373,35 +283,41 @@
|
||||
}
|
||||
|
||||
.plus {
|
||||
font-size: 34px;
|
||||
font-size: 2.125rem;
|
||||
color: #a5a1ac;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dollar {
|
||||
margin-bottom: 1.5em;
|
||||
font-size: 32px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 24px;
|
||||
margin-bottom: .8em;
|
||||
margin-left: .5em;
|
||||
font-size: 1.5rem;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 72px;
|
||||
font-size: 4.5rem;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-options {
|
||||
margin-bottom: 4em;
|
||||
margin-bottom: 64px;
|
||||
|
||||
h4 {
|
||||
color: #34313a;
|
||||
}
|
||||
|
||||
.purple-box {
|
||||
background-color: #4f2a93;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
width: 200px;
|
||||
height: 215px;
|
||||
@@ -415,7 +331,7 @@
|
||||
|
||||
.name {
|
||||
width: 100px;
|
||||
margin-left: .3em;
|
||||
margin-left: 4.8px;
|
||||
}
|
||||
|
||||
.plus {
|
||||
@@ -440,10 +356,12 @@ import paymentsMixin from '../../mixins/payments';
|
||||
import { mapState } from '@/libs/store';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
import groupPlanCreationModal from '../group-plans/groupPlanCreationModal';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
paymentsButtons,
|
||||
groupPlanCreationModal,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
@@ -492,12 +410,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
launchModal () {
|
||||
this.changePage(this.PAGES.CREATE_GROUP);
|
||||
this.$root.$emit('bv::show::modal', 'group-plan-modal');
|
||||
},
|
||||
changePage (page) {
|
||||
this.activePage = page;
|
||||
window.scrollTo(0, 0);
|
||||
this.$root.$emit('bv::show::modal', 'create-group');
|
||||
},
|
||||
createGroup () {
|
||||
this.changePage(this.PAGES.PAY);
|
||||
|
||||
@@ -247,9 +247,6 @@
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet3') }}
|
||||
</li>
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet4') }}
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
v-if="!user.preferences.sleep"
|
||||
@@ -342,6 +339,7 @@
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
:style="glossary-link"
|
||||
v-html="$t('glossary')"
|
||||
></a>
|
||||
</li>
|
||||
@@ -536,6 +534,21 @@
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
// formats the report a bug link to match the others
|
||||
a:not([href]) {
|
||||
&:not([role=button]) {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a:not([href]):hover {
|
||||
&:not([role=button]) {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.tier1-icon, .tier2-icon {
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,14 @@ export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'tasks' });
|
||||
if (this.notification.data.groupId) {
|
||||
this.$router.push({
|
||||
name: 'groupPlanDetailTaskInformation',
|
||||
params: { groupId: this.notification.data.groupId },
|
||||
});
|
||||
} else {
|
||||
this.$router.push({ name: 'tasks' });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -129,8 +129,6 @@ import GUILD_INVITATION from './notifications/guildInvitation';
|
||||
import PARTY_INVITATION from './notifications/partyInvitation';
|
||||
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
|
||||
import QUEST_INVITATION from './notifications/questInvitation';
|
||||
import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval';
|
||||
import GROUP_TASK_APPROVED from './notifications/groupTaskApproved';
|
||||
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
|
||||
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';
|
||||
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
|
||||
@@ -155,8 +153,6 @@ export default {
|
||||
PARTY_INVITATION,
|
||||
CHALLENGE_INVITATION,
|
||||
QUEST_INVITATION,
|
||||
GROUP_TASK_APPROVAL,
|
||||
GROUP_TASK_APPROVED,
|
||||
GROUP_TASK_ASSIGNED,
|
||||
GROUP_TASK_CLAIMED,
|
||||
UNALLOCATED_STATS_POINTS,
|
||||
@@ -182,7 +178,7 @@ export default {
|
||||
openStatus: undefined,
|
||||
actionableNotifications: [
|
||||
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION', 'GROUP_TASK_APPROVED',
|
||||
'QUEST_INVITATION',
|
||||
],
|
||||
// A list of notifications handled by this component,
|
||||
// listed in the order they should appear in the notifications panel.
|
||||
@@ -196,8 +192,6 @@ export default {
|
||||
'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION',
|
||||
'GROUP_TASK_ASSIGNED',
|
||||
'GROUP_TASK_APPROVAL',
|
||||
'GROUP_TASK_APPROVED',
|
||||
'GROUP_TASK_CLAIMED',
|
||||
'NEW_MYSTERY_ITEMS',
|
||||
'CARD_RECEIVED',
|
||||
|
||||
@@ -32,11 +32,12 @@
|
||||
/>
|
||||
<onboarding-complete />
|
||||
<first-drops />
|
||||
<group-plans-update />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
.introjs-helperNumberLayer, .introjs-bullets {
|
||||
.introjs-helperNumberLayer, .introjs-bullets, .introjs-skipbutton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -62,9 +63,9 @@
|
||||
.npc_justin_textbox {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: -3.6em;
|
||||
top: -55px;
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
height: 51px;
|
||||
background-image: url('~@/assets/images/justin_textbox.png');
|
||||
}
|
||||
}
|
||||
@@ -95,7 +96,7 @@
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12) !important;
|
||||
}
|
||||
|
||||
.introjs-skipbutton.btn-primary, .introjs-donebutton.btn-primary {
|
||||
.introjs-donebutton.btn-primary {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -142,6 +143,7 @@ import loginIncentives from './achievements/login-incentives';
|
||||
import onboardingComplete from './achievements/onboardingComplete';
|
||||
import verifyUsername from './settings/verifyUsername';
|
||||
import firstDrops from './achievements/firstDrops';
|
||||
import groupPlansUpdate from './group-plans/groupPlansUpdateModal';
|
||||
|
||||
const NOTIFICATIONS = {
|
||||
// general notifications
|
||||
@@ -268,6 +270,7 @@ export default {
|
||||
genericAchievement,
|
||||
onboardingComplete,
|
||||
firstDrops,
|
||||
groupPlansUpdate,
|
||||
},
|
||||
mixins: [notifications, guide],
|
||||
data () {
|
||||
@@ -612,7 +615,7 @@ export default {
|
||||
const yesterUtcOffset = yesterDay.utcOffset();
|
||||
|
||||
dailys.forEach(task => {
|
||||
if (task && task.group.approval && task.group.approval.requested) return;
|
||||
if (task.group && task.group.id) return;
|
||||
if (task.completed) return;
|
||||
const due = shouldDo(yesterDay, task, { timezoneUtcOffset: yesterUtcOffset });
|
||||
if (task.yesterDaily && due) this.yesterDailies.push(task);
|
||||
|
||||
@@ -196,7 +196,7 @@ export default {
|
||||
appState.group = pick(this.amazonPayments.groupToCreate, ['_id', 'memberCount', 'name']);
|
||||
} else {
|
||||
appState.newGroup = false;
|
||||
appState.group = pick(this.amazonPayments.group, ['_id', 'memberCount', 'name']);
|
||||
appState.group = pick(this.amazonPayments.group, ['_id', 'memberCount', 'name', 'type']);
|
||||
}
|
||||
} else if (paymentType && paymentType.indexOf('gift-') === 0) {
|
||||
appState.gift = this.amazonPayments.gift;
|
||||
|
||||
@@ -153,14 +153,6 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<h4
|
||||
v-once
|
||||
class="col-12 text-payment mb-3"
|
||||
>
|
||||
{{ $t('choosePaymentMethod') }}
|
||||
</h4>
|
||||
</div>
|
||||
<payments-buttons
|
||||
:disabled="!selectedGemsBlock"
|
||||
:stripe-fn="() => redirectToStripe({ gemsBlock: selectedGemsBlock })"
|
||||
@@ -169,6 +161,19 @@
|
||||
})"
|
||||
:amazon-data="{type: 'single', gemsBlock: selectedGemsBlock}"
|
||||
/>
|
||||
<div
|
||||
v-if="eventName === 'fall_extra_gems' || eventName === 'spooky_extra_gems'"
|
||||
class="d-flex flex-column justify-content-center"
|
||||
>
|
||||
<h4 class="mt-3 mx-auto"> {{ $t('howItWorks') }}</h4>
|
||||
<small class="text-center">
|
||||
{{ $t('gemSaleHow', { eventStartMonth, eventStartOrdinal, eventEndOrdinal }) }}
|
||||
</small>
|
||||
<h4 class="mt-3 mx-auto"> {{ $t('limitations') }}</h4>
|
||||
<small class="text-center">
|
||||
{{ $t('gemSaleLimitations', { eventStartMonth, eventStartOrdinal, eventEndOrdinal }) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
@@ -178,6 +183,13 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#buy-gems {
|
||||
small {
|
||||
color: $gray-100;
|
||||
font-size: 12px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.close-icon svg path {
|
||||
stroke: $purple-400;
|
||||
}
|
||||
@@ -423,6 +435,15 @@ export default {
|
||||
}
|
||||
return '';
|
||||
},
|
||||
eventStartMonth () {
|
||||
return moment(this.currentEvent.start).format('MMMM');
|
||||
},
|
||||
eventStartOrdinal () {
|
||||
return moment(this.currentEvent.start).format('Do');
|
||||
},
|
||||
eventEndOrdinal () {
|
||||
return moment(this.currentEvent.end).format('Do');
|
||||
},
|
||||
isGemsPromoActive () {
|
||||
const currEvt = this.currentEvent;
|
||||
if (currEvt && currEvt.gemsPromo && moment().isBefore(currEvt.end)) {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<b-modal
|
||||
id="payments-success-modal"
|
||||
:title="$t('accountSuspendedTitle')"
|
||||
:hide-footer="isFromBalance"
|
||||
:modal-class="isFromBalance ? ['modal-hidden-footer'] : []"
|
||||
:hide-footer="isFromBalance || paymentData.newGroup"
|
||||
:modal-class="isFromBalance || paymentData.newGroup ? ['modal-hidden-footer'] : []"
|
||||
>
|
||||
<div slot="modal-header">
|
||||
<div class="check-container d-flex align-items-center justify-content-center">
|
||||
@@ -16,15 +16,49 @@
|
||||
<h2>{{ $t(isFromBalance ? 'success' : 'paymentSuccessful') }}</h2>
|
||||
</div>
|
||||
<div slot="modal-footer">
|
||||
<!-- everyone else -->
|
||||
<div
|
||||
v-once
|
||||
v-if="paymentData.paymentType !== 'groupPlan' || paymentData.newGroup"
|
||||
class="small-text"
|
||||
>
|
||||
{{ $t('giftSubscriptionText4') }}
|
||||
</div>
|
||||
<!-- upgradedGroup -->
|
||||
<div
|
||||
v-else
|
||||
class="demographics d-flex flex-column justify-content-center"
|
||||
>
|
||||
<lockable-label
|
||||
:text="$t('groupUse')"
|
||||
class="mx-auto label-text"
|
||||
/>
|
||||
<select-translated-array
|
||||
:items="[
|
||||
'groupParentChildren',
|
||||
'groupCouple',
|
||||
'groupFriends',
|
||||
'groupCoworkers',
|
||||
'groupManager',
|
||||
'groupTeacher'
|
||||
]"
|
||||
class="group-input"
|
||||
:placeholder="'groupUseDefault'"
|
||||
:value="upgradedGroup.demographics"
|
||||
@select="upgradedGroup.demographics = $event"
|
||||
/>
|
||||
<button
|
||||
v-if="!paymentData.newGroup"
|
||||
class="btn btn-primary mx-auto"
|
||||
:disabled="!upgradedGroup.demographics"
|
||||
@click="submit()"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 modal-body-col">
|
||||
<!-- buy gems for self -->
|
||||
<template v-if="paymentData.paymentType === 'gems'">
|
||||
<strong v-once>{{ $t('paymentYouReceived') }}</strong>
|
||||
<div class="details-block gems">
|
||||
@@ -36,6 +70,7 @@
|
||||
<span>{{ paymentData.gemsBlock.gems }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- buy or gift gems to someone else -->
|
||||
<template
|
||||
v-if="paymentData.paymentType === 'gift-gems'
|
||||
|| paymentData.paymentType === 'gift-gems-balance'"
|
||||
@@ -50,12 +85,14 @@
|
||||
<span>{{ paymentData.gift.gems.amount }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- give gift subscription (non-recurring)-->
|
||||
<template v-if="paymentData.paymentType === 'gift-subscription'">
|
||||
<span
|
||||
v-html="$t('paymentYouSentSubscription', {
|
||||
name: paymentData.giftReceiver, months: paymentData.subscription.months})"
|
||||
></span>
|
||||
</template>
|
||||
<!-- buy self subscription (recurring) -->
|
||||
<template v-if="paymentData.paymentType === 'subscription'">
|
||||
<strong v-once>{{ $t('nowSubscribed') }}</strong>
|
||||
<div class="details-block">
|
||||
@@ -65,31 +102,45 @@
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- group plan new or upgraded -->
|
||||
<template v-if="paymentData.paymentType === 'groupPlan'">
|
||||
<span
|
||||
v-html="$t(paymentData.newGroup
|
||||
? 'groupPlanCreated' : 'groupPlanUpgraded', {groupName: paymentData.group.name})"
|
||||
></span>
|
||||
<div class="details-block">
|
||||
<span
|
||||
v-html="$t('paymentSubBilling', {
|
||||
amount: groupPlanCost, months: paymentData.subscription.months})"
|
||||
></span>
|
||||
<div
|
||||
v-if="!paymentData.newGroup || paymentData.newGroup"
|
||||
class=""
|
||||
>
|
||||
<div class="details-block group-billing-date">
|
||||
<span
|
||||
v-html="$t('groupsPaymentSubBilling', { renewalDate })"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="small-text group-auto-renew">
|
||||
<span
|
||||
v-once
|
||||
>{{ $t('groupsPaymentAutoRenew') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- buy self subscription auto renew -->
|
||||
<template
|
||||
v-if="paymentData.paymentType === 'groupPlan'
|
||||
|| paymentData.paymentType === 'subscription'"
|
||||
v-if="paymentData.paymentType === 'subscription'"
|
||||
>
|
||||
<span
|
||||
v-once
|
||||
class="small-text auto-renew"
|
||||
>{{ $t('paymentAutoRenew') }}</span>
|
||||
</template>
|
||||
<!-- buttons for subscriptions -->
|
||||
<button
|
||||
v-if="paymentData.paymentType !== 'groupPlan'"
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
@click="submit()"
|
||||
>
|
||||
{{ $t('onwards') }}
|
||||
</button>
|
||||
@@ -102,7 +153,7 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#payments-success-modal .modal-md {
|
||||
max-width: 20.5rem;
|
||||
max-width: 448px;
|
||||
}
|
||||
|
||||
#payments-success-modal .modal-content {
|
||||
@@ -114,6 +165,7 @@
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
#payments-success-modal .modal-header {
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
@@ -124,14 +176,14 @@
|
||||
border-bottom: none;
|
||||
|
||||
h2 {
|
||||
color: white;
|
||||
color: $green-1;
|
||||
}
|
||||
|
||||
.check-container {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: #1CA372;
|
||||
background: $green-1;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -139,14 +191,13 @@
|
||||
.check {
|
||||
width: 35.1px;
|
||||
height: 28px;
|
||||
color: white;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
#payments-success-modal .modal-body {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 24px;
|
||||
background: white;
|
||||
padding: 16px 32px 24px 32px;
|
||||
background: $white;
|
||||
|
||||
.modal-body-col {
|
||||
display: flex;
|
||||
@@ -162,9 +213,9 @@
|
||||
.details-block {
|
||||
background: $gray-700;
|
||||
border-radius: 4px;
|
||||
padding: 8px 24px;
|
||||
padding: 8px 16px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
text-align: center;
|
||||
|
||||
@@ -188,6 +239,14 @@
|
||||
color: $orange-10;
|
||||
font-style: normal;
|
||||
}
|
||||
.group-auto-renew {
|
||||
margin: 12px 20px -8px 20px;
|
||||
color: $yellow-5;
|
||||
font-style: normal;
|
||||
}
|
||||
.group-billing-date {
|
||||
width: 269px;
|
||||
}
|
||||
}
|
||||
|
||||
#payments-success-modal .modal-footer {
|
||||
@@ -201,14 +260,49 @@
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.demographics {
|
||||
background-color: $gray-700;
|
||||
|
||||
.label-text {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.group-input {
|
||||
width: 400px !important;
|
||||
margin-top: -24px !important;
|
||||
}
|
||||
.btn {
|
||||
margin-top: 0px;
|
||||
width: 77px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<script>
|
||||
import checkIcon from '@/assets/svg/check.svg';
|
||||
import gemIcon from '@/assets/svg/gem.svg';
|
||||
import { mapState } from '@/libs/store';
|
||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import paymentsMixin from '@/mixins/payments';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
selectTranslatedArray,
|
||||
lockableLabel,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -216,9 +310,14 @@ export default {
|
||||
gem: gemIcon,
|
||||
}),
|
||||
paymentData: {},
|
||||
upgradedGroup: {
|
||||
name: '',
|
||||
demographics: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data', group: 'group.data' }),
|
||||
groupPlanCost () {
|
||||
const sub = this.paymentData.subscription;
|
||||
const memberCount = this.paymentData.group.memberCount || 1;
|
||||
@@ -242,7 +341,17 @@ export default {
|
||||
this.$root.$off('habitica:payments-success');
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
submit () {
|
||||
if (this.paymentData.group && !this.paymentData.newGroup) {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan upgrade',
|
||||
eventAction: 'group plan upgrade',
|
||||
eventCategory: 'behavior',
|
||||
demographics: this.upgradedGroup.demographics,
|
||||
type: this.paymentData.group.type,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
this.paymentData = {};
|
||||
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
|
||||
},
|
||||
|
||||
@@ -387,9 +387,7 @@
|
||||
{{ $t('saveAndConfirm') }}
|
||||
</button>
|
||||
</div>
|
||||
<h5
|
||||
v-if="user.auth.local.email"
|
||||
>
|
||||
<h5 v-if="user.auth.local.has_password">
|
||||
{{ $t('changeEmail') }}
|
||||
</h5>
|
||||
<div
|
||||
|
||||
@@ -94,6 +94,7 @@ export default {
|
||||
'gems',
|
||||
'bugs-features',
|
||||
'world-boss',
|
||||
'group-plans',
|
||||
];
|
||||
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
@@ -105,11 +106,6 @@ export default {
|
||||
wikiTechAssistanceEmail: `mailto:${TECH_ASSISTANCE_EMAIL}`,
|
||||
},
|
||||
visible: hash && headings.includes(hash) ? hash : null,
|
||||
// @TODO webFaqStillNeedHelp: {
|
||||
// linkStart: '[',
|
||||
// linkEnd: '](/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)',
|
||||
// },
|
||||
// "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the <%= linkStart %>Habitica Help guild<%= linkEnd %>! We're happy to help."
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="container-fluid">
|
||||
<h1>Privacy Notice</h1>
|
||||
<p class="strong pagemeta">
|
||||
Last Updated: December 10, 2021
|
||||
Last Updated September 19, 2022: Removed reference to Facebook login, which is no longer supported.
|
||||
</p>
|
||||
<p>
|
||||
HabitRPG, Inc. (“HabitRPG,” “we,” “us,” or “our”) welcomes you. This privacy notice (the “Privacy
|
||||
@@ -39,8 +39,8 @@
|
||||
In connection with the creation of an account on our Platforms, we collect account credentials such as
|
||||
your email, username, and password. We use this account information to create your account, including to
|
||||
verify your identity. We also use this information to manage your account, including your transactions. If
|
||||
you choose to log into your account through Google, Apple or Facebook, we capture and store the User
|
||||
ID and email address connected to the respective account, so we can verify your identity when you log in.
|
||||
you choose to log into your account through Google or Apple, we capture and store the User ID and email
|
||||
address connected to the respective account, so we can verify your identity when you log in.
|
||||
</p>
|
||||
<h3>User Content</h3>
|
||||
<p>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.home-header {
|
||||
background: #6133b4 !important;
|
||||
background: $purple-300 !important;
|
||||
position: static;
|
||||
box-shadow: none !important;
|
||||
height: 100px !important;
|
||||
@@ -46,13 +46,13 @@
|
||||
|
||||
.nav-item a {
|
||||
font-size: 14px !important;
|
||||
color: #d5c8ff !important;
|
||||
color: $purple-600 !important;
|
||||
padding-top: 2.8em !important;
|
||||
}
|
||||
|
||||
.nav-item a:hover {
|
||||
background: transparent !important;
|
||||
color: #fff !important;
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.nav-item .nav-link {
|
||||
@@ -73,20 +73,20 @@
|
||||
}
|
||||
|
||||
.white-header {
|
||||
background: #fff !important;
|
||||
background-color: #fff !important;
|
||||
background: $white !important;
|
||||
background-color: $white !important;
|
||||
|
||||
a {
|
||||
color: #271b3d !important;
|
||||
color: $header-dark-background !important;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #fff !important;
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
|
||||
#purple-footer {
|
||||
background-color: #271b3d;
|
||||
background-color: $header-dark-background;
|
||||
|
||||
.row {
|
||||
margin: 0;
|
||||
@@ -94,19 +94,91 @@
|
||||
|
||||
footer, footer a {
|
||||
background: transparent;
|
||||
color: #d5c8ff;
|
||||
color: $purple-500;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $purple-400;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top-color: $purple-100;
|
||||
}
|
||||
|
||||
.donate-text {
|
||||
color: $purple-500;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #bda8ff;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.social-circle, .btn-contribute {
|
||||
background: #36205d;
|
||||
color: #bda8ff;
|
||||
.colophon {
|
||||
color: $purple-500;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: #bda8ff;
|
||||
.social-circle {
|
||||
background: $purple-50;
|
||||
color: $purple-500;
|
||||
|
||||
.instagram svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.twitter svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.facebook svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.tumblr svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn-contribute {
|
||||
background: $white;
|
||||
box-shadow: none;
|
||||
border-radius: 2px;
|
||||
width: 175px;
|
||||
height: 32px;
|
||||
color: $gray-50;
|
||||
text-align: center;
|
||||
line-height: 1.71;
|
||||
font-weight: bold;
|
||||
font-size: 0.875rem;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
margin: 32px 0 32px 24px;
|
||||
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.text{
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +214,7 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#bottom-wrap.purple-4 {
|
||||
background-color: #271b3d;
|
||||
background-color: #271B3D;
|
||||
}
|
||||
|
||||
#bottom-wrap {
|
||||
|
||||
@@ -1,59 +1,117 @@
|
||||
<template>
|
||||
<div>
|
||||
<approval-modal :task="task" />
|
||||
<div
|
||||
class="gray-100"
|
||||
>
|
||||
<div
|
||||
v-if="showStatus"
|
||||
>
|
||||
<div
|
||||
v-for="completion in completionsList"
|
||||
:key="completion.userId"
|
||||
class="d-flex completion-row"
|
||||
>
|
||||
<div class="control">
|
||||
<div
|
||||
class="inner d-flex justify-content-center align-items-center p-auto"
|
||||
:class="{interactive: iconClass(completion) !== 'lock'}"
|
||||
@click="iconClass(completion) !== 'lock' ? needsWork(completion) : null"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
:class="iconClass(completion)"
|
||||
v-html="icons[iconClass(completion)]"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-75 py-2 info"
|
||||
>
|
||||
<div>
|
||||
<strong> @{{ completion.userName }} </strong>
|
||||
</div>
|
||||
<div
|
||||
v-if='completion.completedDate'
|
||||
:class="{'green-10': completion.completed}"
|
||||
>
|
||||
{{ completion.completedDateString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!(userIsAssigned && task.group.approval.approved
|
||||
&& !task.completed && task.type !== 'habit')"
|
||||
class="claim-bottom-message d-flex align-items-center"
|
||||
>
|
||||
<div
|
||||
class="mr-auto ml-2"
|
||||
v-if="assignedUsersCount > 0"
|
||||
class="svg-icon ml-2 users-icon color"
|
||||
:class="{'green-10': completionsCount === assignedUsersCount}"
|
||||
v-html="icons.users"
|
||||
></div>
|
||||
<div
|
||||
class="mr-auto ml-1 my-auto"
|
||||
:class="{'green-10': completionsCount === assignedUsersCount}"
|
||||
v-html="message"
|
||||
></div>
|
||||
<div
|
||||
v-if="!userIsAssigned && !task.completed"
|
||||
class="ml-auto mr-2"
|
||||
class="d-flex ml-auto mr-1 my-auto"
|
||||
v-if="task.group.assignedUsers && ['daily','todo'].indexOf(task.type) !== -1"
|
||||
>
|
||||
<a
|
||||
class="claim-color"
|
||||
@click="claim()"
|
||||
>{{ $t('claim') }}</a>
|
||||
<span
|
||||
v-if="assignedUsersCount > 1"
|
||||
class="d-flex mr-1 my-auto"
|
||||
>
|
||||
<span
|
||||
class="small-check"
|
||||
v-if="!showStatus && completionsCount"
|
||||
>
|
||||
<div
|
||||
class="svg-icon color"
|
||||
:class="{'green-10': completionsCount === assignedUsersCount}"
|
||||
v-html="icons.check"
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="ml-1 mr-2 my-auto"
|
||||
:class="{'green-10': completionsCount === assignedUsersCount}"
|
||||
v-if="!showStatus && completionsCount"
|
||||
>
|
||||
{{ completionsCount }}/{{ assignedUsersCount }}
|
||||
</span>
|
||||
<a
|
||||
v-if="assignedUsersCount > 1 && !showStatus"
|
||||
class="blue-10"
|
||||
@click="showStatus = !showStatus"
|
||||
>
|
||||
{{ $t('viewStatus') }}
|
||||
</a>
|
||||
<a
|
||||
v-if="showStatus"
|
||||
@click="showStatus = !showStatus"
|
||||
>
|
||||
{{ $t('close') }}
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
v-if="assignedUsersCount === 1 && task.type === 'daily'
|
||||
&& !task.completed && singleAssignLastDone"
|
||||
class="mr-1 d-inline-flex"
|
||||
>
|
||||
<span
|
||||
v-html="icons.lastComplete"
|
||||
v-b-tooltip.hover.bottom="$t('lastCompleted')"
|
||||
class="svg-icon color last-completed mr-1 my-auto"
|
||||
:class="{'gray-200': completionsCount !== assignedUsersCount}"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
:class="{'green-10': completionsCount === assignedUsersCount}"
|
||||
>
|
||||
{{ formattedCompletionTime }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="userIsAssigned && !approvalRequested && !task.completed"
|
||||
class="ml-auto mr-2"
|
||||
>
|
||||
<a
|
||||
class="unclaim-color"
|
||||
@click="unassign()"
|
||||
>{{ $t('removeClaim') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="approvalRequested && userIsManager"
|
||||
class="claim-bottom-message d-flex align-items-center justify-content-around"
|
||||
>
|
||||
<a
|
||||
class="approve-color"
|
||||
@click="approve()"
|
||||
>{{ $t('approveTask') }}</a>
|
||||
<a @click="needsWork()">{{ $t('needsWork') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="multipleApprovalsRequested && userIsManager"
|
||||
class="claim-bottom-message d-flex align-items-center"
|
||||
>
|
||||
<a @click="showRequests()">{{ $t('viewRequests') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="userIsAssigned && task.group.approval.approved
|
||||
&& !task.completed && task.type !== 'habit'"
|
||||
class="claim-bottom-message d-flex align-items-center justify-content-around"
|
||||
>
|
||||
<a
|
||||
class="approve-color"
|
||||
@click="$emit('claimRewards')"
|
||||
>{{ $t('claimRewards') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,148 +119,220 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
.claim-bottom-message {
|
||||
background-color: $gray-700;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
color: $gray-200;
|
||||
background-color: $gray-600;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.334;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
z-index: 9;
|
||||
height: 24px;
|
||||
|
||||
.blue-10 {
|
||||
color: $blue-10;
|
||||
}
|
||||
}
|
||||
|
||||
.approve-color {
|
||||
color: $green-10 !important;
|
||||
.completion-row {
|
||||
height: 3rem;
|
||||
background-color: $gray-700;
|
||||
font-size: 12px;
|
||||
|
||||
.control {
|
||||
background-color: $gray-200;
|
||||
border-top: 1px solid $gray-100;
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
padding: 9px 6px;
|
||||
|
||||
.inner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 6px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba($white, 0.5);
|
||||
cursor: default;
|
||||
|
||||
&.interactive {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($white, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: $gray-10;
|
||||
|
||||
&.lock {
|
||||
width: 10px;
|
||||
}
|
||||
&.check {
|
||||
margin-left: 2px;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
border-top: 1px solid $gray-600;
|
||||
}
|
||||
}
|
||||
.claim-color {
|
||||
color: $blue-10 !important;
|
||||
|
||||
.gray-100 {
|
||||
color: $gray-100;
|
||||
}
|
||||
.unclaim-color {
|
||||
color: $red-50 !important;
|
||||
.green-10 {
|
||||
color: $green-10;
|
||||
}
|
||||
|
||||
.last-completed {
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.small-check {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: $gray-500;
|
||||
|
||||
.svg-icon {
|
||||
width: 8px;
|
||||
height: 6px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.users-icon {
|
||||
width: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import moment from 'moment';
|
||||
import reduce from 'lodash/reduce';
|
||||
import { mapState } from '@/libs/store';
|
||||
import approvalModal from './approvalModal';
|
||||
import sync from '@/mixins/sync';
|
||||
import checkIcon from '@/assets/svg/check.svg';
|
||||
import lockIcon from '@/assets/svg/lock.svg';
|
||||
import usersIcon from '@/assets/svg/users.svg';
|
||||
import lastComplete from '@/assets/svg/last-complete.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
approvalModal,
|
||||
},
|
||||
mixins: [sync],
|
||||
props: ['task', 'group'],
|
||||
data () {
|
||||
return {
|
||||
showStatus: false,
|
||||
icons: Object.freeze({
|
||||
check: checkIcon,
|
||||
lastComplete,
|
||||
lock: lockIcon,
|
||||
users: usersIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
userIsAssigned () {
|
||||
return this.task.group.assignedUsers
|
||||
&& this.task.group.assignedUsers.indexOf(this.user._id) !== -1;
|
||||
},
|
||||
message () {
|
||||
const { assignedUsers } = this.task.group;
|
||||
const assignedUsersNames = [];
|
||||
const assignedUsersLength = assignedUsers.length;
|
||||
|
||||
// @TODO: Eh, I think we only ever display one user name
|
||||
if (this.group && this.group.members) {
|
||||
assignedUsers.forEach(userId => {
|
||||
const index = findIndex(this.group.members, member => member._id === userId);
|
||||
const assignedMember = this.group.members[index];
|
||||
assignedUsersNames.push(`@${assignedMember.auth.local.username}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (assignedUsersLength === 1 && !this.userIsAssigned) {
|
||||
return this.$t('assignedToUser', { userName: assignedUsersNames[0] });
|
||||
} if (assignedUsersLength > 1 && !this.userIsAssigned) {
|
||||
return this.$t('assignedToMembers', { userCount: assignedUsersLength });
|
||||
} if (assignedUsersLength > 1 && this.userIsAssigned) {
|
||||
return this.$t('assignedToYouAndMembers', { userCount: assignedUsersLength - 1 });
|
||||
} if (this.userIsAssigned) {
|
||||
return this.$t('youAreAssigned');
|
||||
} // if (assignedUsersLength === 0) {
|
||||
return this.$t('taskIsUnassigned');
|
||||
return this.task.group.assignedUsersDetail
|
||||
&& Boolean(this.task.group.assignedUsersDetail[this.user._id]);
|
||||
},
|
||||
userIsManager () {
|
||||
if (
|
||||
this.group
|
||||
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id])
|
||||
) return true;
|
||||
return false;
|
||||
return this.group
|
||||
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id]);
|
||||
},
|
||||
approvalRequested () {
|
||||
if (
|
||||
(this.task.approvals && this.task.approvals.length === 1)
|
||||
|| (this.task.group && this.task.group.approval && this.task.group.approval.requested)
|
||||
) {
|
||||
return true;
|
||||
assignedUsersCount () {
|
||||
return this.task.group.assignedUsers.length;
|
||||
},
|
||||
completionsCount () {
|
||||
return reduce(this.task.group.assignedUsersDetail, (count, assignment) => {
|
||||
if (assignment.completed) return count + 1;
|
||||
return count;
|
||||
}, 0);
|
||||
},
|
||||
completionsList () {
|
||||
if (this.assignedUsersCount === 1) return [];
|
||||
const completionsArray = [];
|
||||
for (const userId of this.task.group.assignedUsers) {
|
||||
if (userId !== this.user._id) {
|
||||
const { completedDate } = this.task.group.assignedUsersDetail[userId];
|
||||
let completedDateString;
|
||||
if (moment().diff(completedDate, 'days') > 0) {
|
||||
completedDateString = `Completed ${moment(completedDate).format('M/D/YY')}`;
|
||||
} else {
|
||||
completedDateString = `Completed at ${moment(completedDate).format('h:mm A')}`;
|
||||
}
|
||||
completionsArray.push({
|
||||
userId,
|
||||
userName: this.task.group.assignedUsersDetail[userId].assignedUsername,
|
||||
completed: this.task.group.assignedUsersDetail[userId].completed,
|
||||
completedDate,
|
||||
completedDateString,
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return completionsArray;
|
||||
},
|
||||
multipleApprovalsRequested () {
|
||||
if (this.task.approvals && this.task.approvals.length > 1) return true;
|
||||
return false;
|
||||
message () {
|
||||
if (this.assignedUsersCount > 1) { // Multi assigned
|
||||
if (this.userIsAssigned) {
|
||||
return this.$t('assignedToYouAndMembers', { userCount: this.assignedUsersCount - 1 });
|
||||
}
|
||||
return this.$t('assignedToMembers', { userCount: this.assignedUsersCount });
|
||||
}
|
||||
if (this.assignedUsersCount === 1) { // Singly assigned
|
||||
const userId = this.task.group.assignedUsers[0];
|
||||
const userName = this.task.group.assignedUsersDetail[userId].assignedUsername;
|
||||
|
||||
if (this.task.group.assignedUsersDetail[userId].completed) { // completed
|
||||
const { completedDate } = this.task.group.assignedUsersDetail[userId];
|
||||
if (this.userIsAssigned) {
|
||||
if (moment().diff(completedDate, 'days') > 0) {
|
||||
return `<strong>You</strong> completed ${moment(completedDate).format('M/D/YY')}`;
|
||||
}
|
||||
return `<strong>You</strong> completed at ${moment(completedDate).format('h:mm A')}`;
|
||||
}
|
||||
if (moment().diff(completedDate, 'days') > 0) {
|
||||
return `@${userName} completed ${moment(completedDate).format('M/D/YY')}`;
|
||||
}
|
||||
return `@${userName} completed at ${moment(completedDate).format('h:mm A')}`;
|
||||
}
|
||||
if (this.userIsAssigned) {
|
||||
return this.$t('youEmphasized');
|
||||
}
|
||||
return `@${userName}`;
|
||||
}
|
||||
return this.$t('error'); // task is open, or the other conditions aren't hitting right
|
||||
},
|
||||
singleAssignLastDone () {
|
||||
const completion = this.task?.group?.assignedUsersDetail[this.user._id];
|
||||
if (completion) return completion.completedDate;
|
||||
return null;
|
||||
},
|
||||
formattedCompletionTime () {
|
||||
if (!this.singleAssignLastDone) return '';
|
||||
if (moment().diff(this.singleAssignLastDone, 'days') < 1) {
|
||||
return moment(this.singleAssignLastDone).format('h:mm A');
|
||||
}
|
||||
return moment(this.singleAssignLastDone).format('M/D/YY');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async claim () {
|
||||
let taskId = this.task._id;
|
||||
// If we are on the user task
|
||||
if (this.task.userId) {
|
||||
taskId = this.task.group.taskId;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId,
|
||||
userId: this.user._id,
|
||||
});
|
||||
this.task.group.assignedUsers.push(this.user._id);
|
||||
this.sync();
|
||||
iconClass (completion) {
|
||||
if (this.userIsManager && completion.completed) return 'check';
|
||||
return 'lock';
|
||||
},
|
||||
async unassign () {
|
||||
if (!window.confirm(this.$t('confirmUnClaim'))) return; // eslint-disable-line no-alert
|
||||
|
||||
let taskId = this.task._id;
|
||||
// If we are on the user task
|
||||
if (this.task.userId) {
|
||||
taskId = this.task.group.taskId;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('tasks:unassignTask', {
|
||||
taskId,
|
||||
userId: this.user._id,
|
||||
});
|
||||
const index = this.task.group.assignedUsers.indexOf(this.user._id);
|
||||
this.task.group.assignedUsers.splice(index, 1);
|
||||
|
||||
this.sync();
|
||||
},
|
||||
approve () {
|
||||
const userIdToApprove = this.task.group.assignedUsers[0];
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdToApprove,
|
||||
});
|
||||
this.task.group.assignedUsers.splice(0, 1);
|
||||
this.task.approvals.splice(0, 1);
|
||||
|
||||
this.sync();
|
||||
},
|
||||
needsWork () {
|
||||
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
|
||||
const userIdNeedsMoreWork = this.task.group.assignedUsers[0];
|
||||
needsWork (completion) {
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdNeedsMoreWork,
|
||||
userId: completion.userId,
|
||||
});
|
||||
this.task.approvals.splice(0, 1);
|
||||
|
||||
this.sync();
|
||||
},
|
||||
showRequests () {
|
||||
this.$root.$emit('bv::show::modal', 'approval-modal');
|
||||
this.task.group.assignedUsersDetail[completion.userId].completed = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="approval-modal"
|
||||
:title="$t('approveTask')"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<div
|
||||
v-for="(approval, index) in task.approvals"
|
||||
class="row approval"
|
||||
>
|
||||
<div class="col-8">
|
||||
<strong>{{ approval.userId.profile.name }}</strong>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="approve(index)"
|
||||
>
|
||||
{{ $t('approve') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="needsWork(index)"
|
||||
>
|
||||
{{ $t('needsWork') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('close') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.row.approval {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['task'],
|
||||
methods: {
|
||||
approve (index) {
|
||||
const userIdToApprove = this.task.group.assignedUsers[index];
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdToApprove,
|
||||
});
|
||||
this.task.group.assignedUsers.splice(index, 1);
|
||||
this.task.approvals.splice(index, 1);
|
||||
},
|
||||
needsWork (index) {
|
||||
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
|
||||
const userIdNeedsMoreWork = this.task.group.assignedUsers[index];
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdNeedsMoreWork,
|
||||
});
|
||||
this.task.approvals.splice(index, 1);
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'approval-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -43,7 +43,7 @@
|
||||
class="tasks-list"
|
||||
>
|
||||
<textarea
|
||||
v-if="isUser"
|
||||
v-if="isUser || canCreateTasks()"
|
||||
ref="quickAdd"
|
||||
v-model="quickAddText"
|
||||
class="quick-add"
|
||||
@@ -102,6 +102,7 @@
|
||||
:group="group"
|
||||
:challenge="challenge"
|
||||
@editTask="editTask"
|
||||
@taskSummary="taskSummary"
|
||||
@moveTo="moveTo"
|
||||
@taskDestroyed="taskDestroyed"
|
||||
/>
|
||||
@@ -347,18 +348,19 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import draggable from 'vuedraggable';
|
||||
import { shouldDo } from '@/../../common/script/cron';
|
||||
import inAppRewards from '@/../../common/script/libs/inAppRewards';
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import Task from './task';
|
||||
import ClearCompletedTodos from './clearCompletedTodos';
|
||||
import buyMixin from '@/mixins/buy';
|
||||
import sync from '@/mixins/sync';
|
||||
import { mapState, mapActions, mapGetters } from '@/libs/store';
|
||||
import shopItem from '../shops/shopItem';
|
||||
import BuyQuestModal from '@/components/shops/quests/buyQuestModal.vue';
|
||||
import PinBadge from '@/components/ui/pinBadge';
|
||||
|
||||
import notifications from '@/mixins/notifications';
|
||||
import { shouldDo } from '@/../../common/script/cron';
|
||||
import inAppRewards from '@/../../common/script/libs/inAppRewards';
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
|
||||
import {
|
||||
getTypeLabel,
|
||||
@@ -382,7 +384,7 @@ export default {
|
||||
shopItem,
|
||||
draggable,
|
||||
},
|
||||
mixins: [buyMixin, notifications],
|
||||
mixins: [buyMixin, notifications, sync],
|
||||
// @TODO Set default values for props
|
||||
// allows for better control of props values
|
||||
// allows for better control of where this component is called
|
||||
@@ -542,6 +544,7 @@ export default {
|
||||
...mapActions({
|
||||
loadCompletedTodos: 'tasks:fetchCompletedTodos',
|
||||
createTask: 'tasks:create',
|
||||
createGroupTasks: 'tasks:createGroupTasks',
|
||||
}),
|
||||
async taskSorted (data) {
|
||||
const filteredList = this.taskList;
|
||||
@@ -574,18 +577,25 @@ export default {
|
||||
},
|
||||
async moveTo (task, where) { // where is 'top' or 'bottom'
|
||||
const taskIdToMove = task._id;
|
||||
const list = this.getUnfilteredTaskList(this.type);
|
||||
const list = this.taskListOverride || this.getUnfilteredTaskList(this.type);
|
||||
|
||||
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
|
||||
const moved = list.splice(oldPosition, 1);
|
||||
const newPosition = where === 'top' ? 0 : list.length;
|
||||
list.splice(newPosition, 0, moved[0]);
|
||||
|
||||
const newOrder = await this.$store.dispatch('tasks:move', {
|
||||
taskId: taskIdToMove,
|
||||
position: newPosition,
|
||||
});
|
||||
this.user.tasksOrder[`${this.type}s`] = newOrder;
|
||||
if (!this.isUser) {
|
||||
await this.$store.dispatch('tasks:moveGroupTask', {
|
||||
taskId: taskIdToMove,
|
||||
position: newPosition,
|
||||
});
|
||||
} else {
|
||||
const newOrder = await this.$store.dispatch('tasks:move', {
|
||||
taskId: taskIdToMove,
|
||||
position: newPosition,
|
||||
});
|
||||
this.user.tasksOrder[`${this.type}s`] = newOrder;
|
||||
}
|
||||
},
|
||||
async rewardSorted (data) {
|
||||
const rewardsList = this.inAppRewards;
|
||||
@@ -606,7 +616,12 @@ export default {
|
||||
this.showPopovers = true;
|
||||
this.isDragging(false);
|
||||
},
|
||||
quickAdd (ev) {
|
||||
canCreateTasks () {
|
||||
if (!this.group) return false;
|
||||
return (this.group.leader && this.group.leader._id === this.user._id)
|
||||
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
|
||||
},
|
||||
async quickAdd (ev) {
|
||||
// Add a new line if Shift+Enter Pressed
|
||||
if (ev.shiftKey) {
|
||||
this.quickAddRows += 1;
|
||||
@@ -620,19 +635,27 @@ export default {
|
||||
|
||||
const tasks = text.split('\n').reverse().filter(taskText => (!!taskText)).map(taskText => {
|
||||
const task = taskDefaults({ type: this.type, text: taskText }, this.user);
|
||||
task.tags = this.selectedTags.slice();
|
||||
if (this.isUser) task.tags = this.selectedTags.slice();
|
||||
return task;
|
||||
});
|
||||
|
||||
this.quickAddText = '';
|
||||
this.quickAddRows = 1;
|
||||
this.createTask(tasks);
|
||||
if (this.group) {
|
||||
await this.createGroupTasks({ groupId: this.group.id, tasks });
|
||||
this.sync();
|
||||
} else {
|
||||
this.createTask(tasks);
|
||||
}
|
||||
this.$refs.quickAdd.blur();
|
||||
return true;
|
||||
},
|
||||
editTask (task) {
|
||||
this.$emit('editTask', task);
|
||||
},
|
||||
taskSummary (task) {
|
||||
this.$emit('taskSummary', task);
|
||||
},
|
||||
activateFilter (type, filter = '') {
|
||||
// Needs a separate API call as this data may not reside in store
|
||||
if (type === 'todo' && filter === 'complete2') {
|
||||
@@ -687,7 +710,7 @@ export default {
|
||||
filterByLabel (taskList, type, filter) {
|
||||
if (!taskList) return [];
|
||||
const selectedFilter = getActiveFilter(type, filter, this.challenge);
|
||||
return sortAndFilterTasks(taskList, selectedFilter);
|
||||
return sortAndFilterTasks(taskList, selectedFilter, Boolean(this.group));
|
||||
},
|
||||
filterByTagList (taskList, tagList = []) {
|
||||
let filteredTaskList = taskList;
|
||||
|
||||
@@ -1,80 +1,98 @@
|
||||
<template>
|
||||
<div class="checklist-component">
|
||||
<lockable-label
|
||||
:locked="disabled || disableItems"
|
||||
:text="$t('checklist')"
|
||||
/>
|
||||
<draggable
|
||||
v-model="checklist"
|
||||
:options="{
|
||||
handle: '.grippy',
|
||||
filter: '.task-dropdown',
|
||||
disabled: disabled,
|
||||
}"
|
||||
@update="updateChecklist"
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<lockable-label
|
||||
:locked="disabled"
|
||||
:text="$t('checklist')"
|
||||
/>
|
||||
<div
|
||||
v-for="(item, $index) in checklist"
|
||||
:key="item.id"
|
||||
class="inline-edit-input-group checklist-group input-group"
|
||||
class="svg-icon icon-16 my-auto ml-auto pointer"
|
||||
:class="{'chevron-flip': showChecklist}"
|
||||
v-html="icons.chevron"
|
||||
@click="showChecklist = !showChecklist"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<b-collapse
|
||||
id="checklistCollapse"
|
||||
v-model="showChecklist"
|
||||
>
|
||||
<draggable
|
||||
v-model="checklist"
|
||||
:options="{
|
||||
handle: '.grippy',
|
||||
filter: '.task-dropdown',
|
||||
disabled: disabled,
|
||||
}"
|
||||
@update="updateChecklist"
|
||||
>
|
||||
<div
|
||||
v-for="(item, $index) in checklist"
|
||||
:key="item.id"
|
||||
class="inline-edit-input-group checklist-group input-group"
|
||||
>
|
||||
<span
|
||||
v-if="!disabled && !disableDrag"
|
||||
class="grippy"
|
||||
v-html="icons.grip"
|
||||
>
|
||||
</span>
|
||||
|
||||
<checkbox
|
||||
v-if="!disableEdit"
|
||||
:id="`checklist-${item.id}`"
|
||||
:checked.sync="item.completed"
|
||||
:disabled="disabled"
|
||||
class="input-group-prepend"
|
||||
:class="{'cursor-auto': disabled}"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-model="item.text"
|
||||
class="inline-edit-input checklist-item form-control"
|
||||
type="text"
|
||||
:disabled="disabled || disableEdit"
|
||||
:class="summaryClass(item)"
|
||||
>
|
||||
<span
|
||||
v-if="!disabled && !disableEdit"
|
||||
class="input-group-append"
|
||||
@click="removeChecklistItem($index)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon destroy-icon"
|
||||
v-html="icons.destroy"
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="!disabled && !disableEdit"
|
||||
class="inline-edit-input-group checklist-group input-group new-checklist"
|
||||
:class="{'top-border': items.length === 0}"
|
||||
>
|
||||
<span
|
||||
v-if="!disabled && !disableDrag"
|
||||
class="grippy"
|
||||
v-html="icons.grip"
|
||||
v-once
|
||||
class="input-group-prepend new-icon"
|
||||
v-html="icons.positive"
|
||||
>
|
||||
</span>
|
||||
|
||||
<checkbox
|
||||
:id="`checklist-${item.id}`"
|
||||
:checked.sync="item.completed"
|
||||
:disabled="disabled || disableItems"
|
||||
class="input-group-prepend"
|
||||
:class="{'cursor-auto': disabled || disableItems}"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-model="item.text"
|
||||
v-model="newChecklistItem"
|
||||
class="inline-edit-input checklist-item form-control"
|
||||
type="text"
|
||||
:disabled="disabled || disableItems"
|
||||
:placeholder="$t('newChecklistItem')"
|
||||
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
|
||||
@keyup.enter="addChecklistItem($event, true)"
|
||||
@blur="addChecklistItem($event, false)"
|
||||
>
|
||||
<span
|
||||
v-if="!disabled && !disableItems"
|
||||
class="input-group-append"
|
||||
@click="removeChecklistItem($index)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon destroy-icon"
|
||||
v-html="icons.destroy"
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="!disabled && !disableItems"
|
||||
class="inline-edit-input-group checklist-group input-group new-checklist"
|
||||
:class="{'top-border': items.length === 0}"
|
||||
>
|
||||
<span
|
||||
v-once
|
||||
class="input-group-prepend new-icon"
|
||||
v-html="icons.positive"
|
||||
>
|
||||
</span>
|
||||
|
||||
<input
|
||||
v-model="newChecklistItem"
|
||||
class="inline-edit-input checklist-item form-control"
|
||||
type="text"
|
||||
:placeholder="$t('newChecklistItem')"
|
||||
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
|
||||
@keyup.enter="addChecklistItem($event, true)"
|
||||
@blur="addChecklistItem($event, false)"
|
||||
>
|
||||
</div>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,7 +122,7 @@ export default {
|
||||
disableDrag: {
|
||||
type: Boolean,
|
||||
},
|
||||
disableItems: {
|
||||
disableEdit: {
|
||||
type: Boolean,
|
||||
},
|
||||
items: {
|
||||
@@ -114,6 +132,7 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
checklist: this.items,
|
||||
showChecklist: true,
|
||||
hasPossibilityOfIMEConversion: true,
|
||||
newChecklistItem: null,
|
||||
icons: Object.freeze({
|
||||
@@ -125,6 +144,11 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
summaryClass (item) {
|
||||
if (!this.disableEdit) return '';
|
||||
if (item.completed) return 'summary-completed';
|
||||
return 'summary-incomplete';
|
||||
},
|
||||
updateChecklist () {
|
||||
this.$emit('update:items', this.checklist);
|
||||
},
|
||||
@@ -166,14 +190,22 @@ export default {
|
||||
|
||||
.checklist-component {
|
||||
|
||||
.top-border {
|
||||
border-top: 1px solid $gray-500;
|
||||
.chevron-flip {
|
||||
transform: translateY(-5px) rotate(180deg);
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.top-border {
|
||||
border-top: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.checklist-group {
|
||||
height: 2rem;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
@@ -251,6 +283,13 @@ export default {
|
||||
border-radius: 0px;
|
||||
border: none !important;
|
||||
padding-left: 36px;
|
||||
|
||||
&.summary-incomplete {
|
||||
opacity: 1;
|
||||
}
|
||||
&.summary-completed {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-group {
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
:class="{ 'break': maxItems === 0 }"
|
||||
>
|
||||
<template v-if="items.length === 0">
|
||||
<div class="items-none">
|
||||
<div class="items-none mb-1">
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="item in truncatedSelectedItems"
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:title="item.name"
|
||||
class="multi-item mr-1 d-inline-flex align-items-center"
|
||||
class="multi-item mr-1 mb-1 d-inline-flex align-items-center"
|
||||
:class="{'margin-adjust': maxItems !== 0, 'pill-invert': pillInvert}"
|
||||
|
||||
@click.stop="removeItem($event, item)"
|
||||
@@ -27,12 +27,6 @@
|
||||
v-html="icons.remove"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="remainingSelectedItems.length > 0"
|
||||
class="items-more ml-75"
|
||||
>
|
||||
+{{ remainingSelectedItems.length }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
multi<template>
|
||||
<template>
|
||||
<div>
|
||||
<b-dropdown
|
||||
ref="dropdown"
|
||||
@@ -29,6 +29,7 @@ multi<template>
|
||||
</b-dropdown-header>
|
||||
<template v-slot:button-content>
|
||||
<multi-list
|
||||
class="d-flex flex-wrap"
|
||||
:items="selectedItemsAsObjects"
|
||||
:add-new="addNew"
|
||||
:pill-invert="pillInvert"
|
||||
@@ -85,6 +86,13 @@ multi<template>
|
||||
|
||||
$itemHeight: 2rem;
|
||||
|
||||
.inline-dropdown {
|
||||
&.select-multi .dropdown-toggle {
|
||||
height: auto;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-multi {
|
||||
.dropdown-toggle {
|
||||
padding-left: 0.75rem;
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-dropdown
|
||||
ref="dropdown"
|
||||
class="inline-dropdown select-multi"
|
||||
:toggle-class="isOpened ? 'active' : null"
|
||||
:class="{'margin-adjust': selectedItem}"
|
||||
@show="wasOpened()"
|
||||
@hide="hideCallback($event)"
|
||||
@toggle="openOrClose($event)"
|
||||
>
|
||||
<b-dropdown-header>
|
||||
<div class="mb-2">
|
||||
<b-form-input
|
||||
v-model="search"
|
||||
type="text"
|
||||
:placeholder="searchPlaceholder"
|
||||
@keyup.enter="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
</b-dropdown-header>
|
||||
<template v-slot:button-content>
|
||||
<div
|
||||
class="mr-1 d-inline-flex align-items-center"
|
||||
@click.stop="selectItem({id: selectedItem})"
|
||||
v-markdown="
|
||||
allItemsMap[selectedItem] ? `@${allItemsMap[selectedItem].name}`
|
||||
: emptyMessage
|
||||
"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="addNew || availableToSelect.length > 0"
|
||||
:class="{
|
||||
'item-group': true,
|
||||
'add-new': availableToSelect.length === 0 && search !== '',
|
||||
'scroll': availableToSelect.length > 5
|
||||
}"
|
||||
>
|
||||
<b-dropdown-item-button
|
||||
v-for="item in availableToSelect"
|
||||
:key="item.id"
|
||||
class="ignore-hide multi-item"
|
||||
:class="{ 'none': item.id === 'none', selectListItem: true }"
|
||||
@click.prevent.stop="selectItem(item)"
|
||||
>
|
||||
<div
|
||||
v-markdown="item.name"
|
||||
class="label"
|
||||
></div>
|
||||
<div
|
||||
v-if="item.addlText"
|
||||
class="addl-text"
|
||||
>
|
||||
{{ item.addlText }}
|
||||
</div>
|
||||
</b-dropdown-item-button>
|
||||
</div>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
$itemHeight: 2rem;
|
||||
|
||||
.selected-item {
|
||||
display: inline-block;
|
||||
height: 1.5rem;
|
||||
border-radius: 100px;
|
||||
background-color: $white;
|
||||
border: solid 1px $gray-400;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
|
||||
.multi-label {
|
||||
height: 1rem;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: normal;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
.select-multi {
|
||||
.dropdown-toggle {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
background-color: $gray-700;
|
||||
padding-bottom: 0;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.dropdown-item, .dropdown-header {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.none {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.multi-item button {
|
||||
height: $itemHeight;
|
||||
display: flex;
|
||||
|
||||
.label {
|
||||
height: 1.5rem;
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.addl-text {
|
||||
height: 1rem;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
text-align: right;
|
||||
color: $gray-100;
|
||||
align-self: center;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.addl-text {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-group {
|
||||
max-height: #{5*$itemHeight};
|
||||
|
||||
&.add-new {
|
||||
height: 30px;
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&.scroll {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: none;
|
||||
height: 2rem;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
color: $gray-100;
|
||||
|
||||
margin-left: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
},
|
||||
props: {
|
||||
addNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allItems: {
|
||||
type: Array,
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
},
|
||||
selectedItem: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
preventHide: true,
|
||||
isOpened: false,
|
||||
selected: this.selectedItem,
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allItemsMap () {
|
||||
const obj = {};
|
||||
this.allItems.forEach(t => {
|
||||
obj[t.id] = t;
|
||||
});
|
||||
return obj;
|
||||
},
|
||||
selectedItemAsObject () {
|
||||
return this.selectedItem ? this.allItemsMap[this.selectedItem] : null;
|
||||
},
|
||||
availableToSelect () {
|
||||
const searchString = this.search.toLowerCase();
|
||||
|
||||
const filteredItems = this.allItems.filter(i => i.name.toLowerCase().includes(searchString));
|
||||
|
||||
return filteredItems;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selected () {
|
||||
this.$emit('changed', this.selectedItem);
|
||||
},
|
||||
},
|
||||
created () {
|
||||
document.addEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
mounted () {
|
||||
this.$refs.dropdown.clickOutHandler = () => {
|
||||
this.closeSelectPopup();
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closeSelectPopup () {
|
||||
this.preventHide = false;
|
||||
this.isOpened = false;
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.dropdown.hide();
|
||||
});
|
||||
},
|
||||
openOrClose ($event) {
|
||||
if (this.isOpened) {
|
||||
this.closeSelectPopup();
|
||||
$event.preventDefault();
|
||||
}
|
||||
},
|
||||
selectItem (item) {
|
||||
if (item.id === this.selectedItem) {
|
||||
this.$emit('toggle', null);
|
||||
} else {
|
||||
this.$emit('toggle', item.id);
|
||||
}
|
||||
this.closeSelectPopup();
|
||||
},
|
||||
hideCallback ($event) {
|
||||
if (this.preventHide) {
|
||||
$event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpened = false;
|
||||
},
|
||||
wasOpened () {
|
||||
this.isOpened = true;
|
||||
this.preventHide = true;
|
||||
},
|
||||
handleEsc (e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.closeSelectPopup();
|
||||
}
|
||||
},
|
||||
handleSubmit () {
|
||||
if (!this.addNew) return;
|
||||
const { search } = this;
|
||||
this.$emit('addNew', search);
|
||||
|
||||
this.search = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -9,6 +9,7 @@
|
||||
:right="right"
|
||||
:hide-icon="false"
|
||||
:inline-dropdown="inlineDropdown"
|
||||
:placeholder="placeholder"
|
||||
@select="selectItem($event)"
|
||||
>
|
||||
<template v-slot:item="{ item }">
|
||||
@@ -70,6 +71,9 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -68,7 +68,8 @@
|
||||
</b-popover>
|
||||
<div
|
||||
class="spell-border"
|
||||
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl }"
|
||||
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl,
|
||||
'insufficient-mana': user.stats.mp < skill.mana }"
|
||||
>
|
||||
<div
|
||||
class="spell"
|
||||
@@ -87,19 +88,6 @@
|
||||
<div>Level {{ skill.lvl }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="spellDisabled(key) === true"
|
||||
class="mana"
|
||||
>
|
||||
<div class="mana-text">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.mana"
|
||||
></div>
|
||||
<div>{{ skill.mana }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="mana"
|
||||
@@ -200,7 +188,7 @@
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
&:hover:not(.disabled):not(.insufficient-mana) {
|
||||
background-color: $purple-400;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16),
|
||||
@@ -216,6 +204,10 @@
|
||||
background-color: rgba(26, 24, 29, 0.5);
|
||||
}
|
||||
|
||||
.mana-text {
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
.level {
|
||||
color: $white;
|
||||
font-weight: normal;
|
||||
@@ -223,6 +215,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.insufficient-mana:not(.disabled) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.spell {
|
||||
background: $white;
|
||||
border-radius: 4px;
|
||||
@@ -465,7 +461,7 @@ export default {
|
||||
spellDisabled (skill) {
|
||||
const incompleteDailiesDue = this
|
||||
.getUnfilteredTaskList('daily')
|
||||
.filter(daily => !daily.completed && daily.isDue)
|
||||
.filter(daily => !daily.completed && !daily.group.id && daily.isDue)
|
||||
.length;
|
||||
|
||||
if (skill === 'frost' && this.user.stats.buffs.streaks) {
|
||||
@@ -481,7 +477,9 @@ export default {
|
||||
skillNotes (skill) {
|
||||
let notes = skill.notes();
|
||||
|
||||
if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
|
||||
if (this.user.stats.lvl < skill.lvl) {
|
||||
notes = this.$t('spellLevelTooHigh', { level: skill.lvl });
|
||||
} else if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
|
||||
notes = this.$t('spellAlreadyCast');
|
||||
} else if (skill.key === 'stealth' && this.spellDisabled(skill.key)) {
|
||||
notes = this.$t('spellAlreadyCast');
|
||||
|
||||
@@ -3,47 +3,39 @@
|
||||
<div
|
||||
class="task transition"
|
||||
:class="[{
|
||||
'groupTask': task.group.id,
|
||||
'task-not-editable': !teamManagerAccess},
|
||||
`type_${task.type}`
|
||||
'groupTask': task.group.id,
|
||||
'task-not-editable': !teamManagerAccess,
|
||||
'task-not-scoreable': showTaskLockIcon,
|
||||
}, `type_${task.type}`
|
||||
]"
|
||||
@click="castEnd($event, task)"
|
||||
>
|
||||
<approval-header
|
||||
v-if="task.group.id"
|
||||
:task="task"
|
||||
:group="group"
|
||||
/>
|
||||
<div
|
||||
class="d-flex"
|
||||
:class="{'task-not-scoreable': isUser !== true || task.group.approval.requested
|
||||
&& !(task.group.approval.approved && task.type === 'habit')}"
|
||||
:class="{'task-not-scoreable': showTaskLockIcon }"
|
||||
>
|
||||
<!-- Habits left side control-->
|
||||
<div
|
||||
v-if="task.type === 'habit'"
|
||||
class="left-control d-flex justify-content-center pt-3"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-bottom-box': task.group.id && !isOpenTask,
|
||||
'control-top-box': approvalsClass
|
||||
}, controlClass.up.bg]"
|
||||
>
|
||||
<div
|
||||
class="task-control habit-control"
|
||||
:class="[{
|
||||
'habit-control-positive-enabled': task.up && isUser,
|
||||
'habit-control-positive-disabled': !task.up && isUser,
|
||||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
'habit-control-positive-enabled': task.up && !showTaskLockIcon,
|
||||
'habit-control-positive-disabled': !task.up && !showTaskLockIcon,
|
||||
'task-not-scoreable': showTaskLockIcon,
|
||||
}, controlClass.up.inner]"
|
||||
tabindex="0"
|
||||
@click="(isUser && task.up && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('up') : null"
|
||||
@keypress.enter="(isUser && task.up && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('up') : null"
|
||||
@click="score('up')"
|
||||
@keypress.enter="score('up')"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon lock"
|
||||
:class="task.up ? controlClass.up.icon : 'positive'"
|
||||
v-html="icons.lock"
|
||||
@@ -60,20 +52,21 @@
|
||||
v-if="task.type === 'daily' || task.type === 'todo'"
|
||||
class="left-control d-flex justify-content-center"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-bottom-box': task.group.id && !isOpenTask,
|
||||
'control-top-box': approvalsClass}, controlClass.bg]"
|
||||
>
|
||||
<div
|
||||
class="task-control daily-todo-control"
|
||||
:class="controlClass.inner"
|
||||
:class="[
|
||||
{ 'task-not-scoreable': showTaskLockIcon },
|
||||
controlClass.inner,
|
||||
]"
|
||||
tabindex="0"
|
||||
@click="isUser && !task.group.approval.requested
|
||||
? score(task.completed ? 'down' : 'up' ) : null"
|
||||
@keypress.enter="isUser && !task.group.approval.requested
|
||||
? score(task.completed ? 'down' : 'up' ) : null"
|
||||
@click="score(showCheckIcon ? 'down' : 'up' )"
|
||||
@keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon lock"
|
||||
:class="controlClass.icon"
|
||||
v-html="icons.lock"
|
||||
@@ -82,7 +75,7 @@
|
||||
v-else
|
||||
class="svg-icon check"
|
||||
:class="{
|
||||
'display-check-icon': task.completed || task.group.approval.requested,
|
||||
'display-check-icon': showCheckIcon,
|
||||
[controlClass.checkbox]: true,
|
||||
}"
|
||||
v-html="icons.check"
|
||||
@@ -95,8 +88,8 @@
|
||||
:class="contentClass"
|
||||
>
|
||||
<div
|
||||
class="task-clickable-area"
|
||||
:class="{ 'cursor-auto': !isUser && !teamManagerAccess }"
|
||||
class="task-clickable-area pt-1 pl-75 pb-0"
|
||||
:class="{ 'cursor-auto': !teamManagerAccess }"
|
||||
tabindex="0"
|
||||
@click="edit($event, task)"
|
||||
@keypress.enter="edit($event, task)"
|
||||
@@ -105,14 +98,14 @@
|
||||
<h3
|
||||
v-markdown="task.text"
|
||||
class="task-title markdown"
|
||||
:class="{ 'has-notes': task.notes || (!isUser && task.group.managerNotes)}"
|
||||
:class="{ 'has-notes': task.notes }"
|
||||
></h3>
|
||||
<menu-dropdown
|
||||
v-if="!isRunningYesterdailies && showOptions"
|
||||
ref="taskDropdown"
|
||||
v-b-tooltip.hover.top="$t('options')"
|
||||
tabindex="0"
|
||||
class="task-dropdown"
|
||||
class="task-dropdown mr-1"
|
||||
:right="task.type === 'reward'"
|
||||
>
|
||||
<div slot="dropdown-toggle">
|
||||
@@ -138,7 +131,6 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUser"
|
||||
class="dropdown-item"
|
||||
tabindex="0"
|
||||
@click="moveToTop"
|
||||
@@ -153,7 +145,6 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUser"
|
||||
class="dropdown-item"
|
||||
tabindex="0"
|
||||
@click="moveToBottom"
|
||||
@@ -186,7 +177,7 @@
|
||||
</menu-dropdown>
|
||||
</div>
|
||||
<div
|
||||
v-markdown="displayNotes"
|
||||
v-markdown="task.notes"
|
||||
class="task-notes small-text"
|
||||
:class="{'has-checklist': task.notes && hasChecklist}"
|
||||
></div>
|
||||
@@ -220,7 +211,7 @@
|
||||
v-if="!task.collapseChecklist"
|
||||
:key="item.id"
|
||||
class="custom-control custom-checkbox checklist-item"
|
||||
:class="{'checklist-item-done': item.completed, 'cursor-auto': !isUser}"
|
||||
:class="{'checklist-item-done': item.completed, 'cursor-auto': showTaskLockIcon}"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<input
|
||||
@@ -229,14 +220,14 @@
|
||||
tabindex="0"
|
||||
type="checkbox"
|
||||
:checked="item.completed"
|
||||
:disabled="castingSpell || !isUser"
|
||||
:disabled="castingSpell || showTaskLockIcon"
|
||||
@change="toggleChecklistItem(item)"
|
||||
@keypress.enter="toggleChecklistItem(item)"
|
||||
>
|
||||
<label
|
||||
v-markdown="item.text"
|
||||
class="custom-control-label"
|
||||
:class="{ 'cursor-auto': !isUser }"
|
||||
:class="{ 'cursor-auto': showTaskLockIcon }"
|
||||
:for="`checklist-${item.id}-${random}`"
|
||||
></label>
|
||||
</div>
|
||||
@@ -316,7 +307,7 @@
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasTags"
|
||||
v-if="hasTags && !task.group.id"
|
||||
:id="`tags-icon-${task._id}`"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
@@ -326,7 +317,7 @@
|
||||
></div>
|
||||
</div>
|
||||
<b-popover
|
||||
v-if="hasTags"
|
||||
v-if="hasTags && !task.group.id"
|
||||
:target="`tags-icon-${task._id}`"
|
||||
triggers="hover"
|
||||
placement="bottom"
|
||||
@@ -356,25 +347,22 @@
|
||||
v-if="task.type === 'habit'"
|
||||
class="right-control d-flex justify-content-center pt-3"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-bottom-box': task.group.id && !isOpenTask,
|
||||
'control-top-box': approvalsClass}, controlClass.down.bg]"
|
||||
>
|
||||
<div
|
||||
class="task-control habit-control"
|
||||
:class="[{
|
||||
'habit-control-negative-enabled': task.down && isUser,
|
||||
'habit-control-negative-disabled': !task.down && isUser,
|
||||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
'habit-control-negative-enabled': task.down && !showTaskLockIcon,
|
||||
'habit-control-negative-disabled': !task.down && !showTaskLockIcon,
|
||||
'task-not-scoreable': showTaskLockIcon,
|
||||
}, controlClass.down.inner]"
|
||||
tabindex="0"
|
||||
@click="(isUser && task.down && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('down') : null"
|
||||
@keypress.enter="(isUser && task.down && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('down') : null"
|
||||
@click="score('down')"
|
||||
@keypress.enter="score('down')"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon lock"
|
||||
:class="task.down ? controlClass.down.icon : 'negative'"
|
||||
v-html="icons.lock"
|
||||
@@ -390,13 +378,22 @@
|
||||
<div
|
||||
v-if="task.type === 'reward'"
|
||||
class="right-control d-flex align-items-center justify-content-center reward-control"
|
||||
:class="controlClass.bg"
|
||||
:class="[
|
||||
{ 'task-not-scoreable': showTaskLockIcon },
|
||||
controlClass.bg,
|
||||
]"
|
||||
tabindex="0"
|
||||
@click="isUser ? score('down') : null"
|
||||
@keypress.enter="isUser ? score('down') : null"
|
||||
@click="score('down')"
|
||||
@keypress.enter="score('down')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon color lock"
|
||||
v-html="icons.lock"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon mb-1"
|
||||
v-html="icons.gold"
|
||||
></div>
|
||||
<div class="small-text">
|
||||
@@ -405,10 +402,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<approval-footer
|
||||
v-if="task.group.id"
|
||||
v-if="task.group.id && !isOpenTask"
|
||||
:task="task"
|
||||
:group="group"
|
||||
@claimRewards="score('up')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,7 +423,7 @@
|
||||
border: $purple-400 solid 1px;
|
||||
|
||||
:not(task-best-control-inner-habit) { // round icon
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,11 +445,11 @@
|
||||
margin-bottom: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&:hover:not(.task-not-editable),
|
||||
&:focus-within:not(.task-not-editable) {
|
||||
&:hover:not(.task-not-editable.task-not-scoreable),
|
||||
&:focus-within:not(.task-not-editable.task-not-scoreable) {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
z-index: 11;
|
||||
}
|
||||
@@ -469,10 +465,10 @@
|
||||
}
|
||||
|
||||
.task.groupTask {
|
||||
&:hover:not(.task-not-editable),
|
||||
&:focus-within:not(.task-not-editable) {
|
||||
&:hover:not(.task-not-editable.task-not-scoreable),
|
||||
&:focus-within:not(.task-not-editable.task-not-scoreable) {
|
||||
border: $purple-400 solid 1px;
|
||||
border-radius: 3px;
|
||||
border-radius: 5px;
|
||||
margin: -1px; // to counter the border width
|
||||
margin-bottom: 1px;
|
||||
transition: none; // with transition, the border color switches from black to $purple-400
|
||||
@@ -495,6 +491,7 @@
|
||||
}
|
||||
|
||||
&.has-notes {
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -508,8 +505,6 @@
|
||||
}
|
||||
|
||||
.task-clickable-area {
|
||||
padding: 7px 8px;
|
||||
padding-bottom: 0px;
|
||||
border: transparent solid 1px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -518,7 +513,7 @@
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
border: $purple-400 solid 1px;
|
||||
}
|
||||
}
|
||||
@@ -575,7 +570,11 @@
|
||||
}
|
||||
|
||||
.task-dropdown {
|
||||
max-height: 18px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-dropdown ::v-deep .dropdown-menu {
|
||||
@@ -632,8 +631,8 @@
|
||||
}
|
||||
|
||||
&.reward-content {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,8 +795,8 @@
|
||||
}
|
||||
}
|
||||
.left-control {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
min-height: 60px;
|
||||
border: 1px solid transparent;
|
||||
border-right: none;
|
||||
@@ -809,15 +808,15 @@
|
||||
.task:not(.type_habit) {
|
||||
.left-control {
|
||||
& + .task-content {
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-control {
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
min-height: 56px;
|
||||
border: 1px solid transparent;
|
||||
border-left: none;
|
||||
@@ -853,8 +852,12 @@
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lock {
|
||||
color: $gray-200;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
margin-top: 4px;
|
||||
font-style: initial;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -920,20 +923,19 @@ import lockIcon from '@/assets/svg/lock.svg';
|
||||
import menuIcon from '@/assets/svg/menu.svg';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import scoreTask from '@/mixins/scoreTask';
|
||||
import approvalHeader from './approvalHeader';
|
||||
import sync from '@/mixins/sync';
|
||||
import approvalFooter from './approvalFooter';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
approvalFooter,
|
||||
approvalHeader,
|
||||
MenuDropdown,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [scoreTask],
|
||||
mixins: [scoreTask, sync],
|
||||
// @TODO: maybe we should store the group on state?
|
||||
props: {
|
||||
task: {},
|
||||
@@ -1064,11 +1066,45 @@ export default {
|
||||
},
|
||||
teamManagerAccess () {
|
||||
if (!this.isGroupTask || !this.group) return true;
|
||||
if (!this.group.leader && !this.group.managers) return false;
|
||||
return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]);
|
||||
},
|
||||
displayNotes () {
|
||||
if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes;
|
||||
return this.task.notes;
|
||||
isOpenTask () {
|
||||
if (!this.isGroupTask) return false;
|
||||
if (this.task?.group?.assignedUsers?.length > 0) return false;
|
||||
return true;
|
||||
},
|
||||
showCheckIcon () {
|
||||
if (this.isGroupTask && this.task.group.assignedUsersDetail
|
||||
&& this.task.group.assignedUsersDetail[this.user._id]) {
|
||||
return this.task.group.assignedUsersDetail[this.user._id].completed;
|
||||
}
|
||||
return this.task.completed;
|
||||
},
|
||||
showTaskLockIcon () {
|
||||
if (this.isUser) return false;
|
||||
if (this.isGroupTask) {
|
||||
if (this.task.completed) {
|
||||
if (this.task.group.assignedUsersDetail
|
||||
&& this.task.group.assignedUsersDetail[this.user._id]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.task.group.completedBy.userId === this.user._id) return false;
|
||||
if (this.teamManagerAccess) {
|
||||
if (!this.task.group.assignedUsers || this.task.group.assignedUsers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (this.task.group.assignedUsers.length === 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (this.isOpenTask) return false;
|
||||
if (this.task.group.assignedUsersDetail[this.user._id]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -1098,7 +1134,7 @@ export default {
|
||||
return this.task.date && this.$t('dueIn', { dueIn });
|
||||
},
|
||||
edit (e, task) {
|
||||
if (this.isRunningYesterdailies || !this.showEdit) return;
|
||||
if (this.isRunningYesterdailies) return;
|
||||
const target = e.target || e.srcElement;
|
||||
|
||||
/*
|
||||
@@ -1116,8 +1152,11 @@ export default {
|
||||
const isEditAction = this.$refs.editTaskItem && this.$refs.editTaskItem.contains(target);
|
||||
|
||||
if (isDropdown && !isEditAction) return;
|
||||
if (this.$store.state.spellOptions.castingSpell) return;
|
||||
|
||||
if (!this.$store.state.spellOptions.castingSpell) {
|
||||
if (!this.showEdit) {
|
||||
this.$emit('taskSummary', task);
|
||||
} else {
|
||||
this.$emit('editTask', task);
|
||||
}
|
||||
},
|
||||
@@ -1137,6 +1176,22 @@ export default {
|
||||
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
|
||||
},
|
||||
async score (direction) {
|
||||
if (this.showTaskLockIcon) return;
|
||||
if (this.task.type === 'habit' && !this.task[direction]) return;
|
||||
if (
|
||||
this.isGroupTask && direction === 'down'
|
||||
&& ['todo', 'daily'].indexOf(this.task.type) !== -1
|
||||
&& !((this.task.group.completedBy && this.task.group.completedBy.userId === this.user._id)
|
||||
|| (this.task.group.assignedUsersDetail
|
||||
&& this.task.group.assignedUsersDetail[this.user._id]))
|
||||
) {
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.task._id,
|
||||
userId: this.task.group.assignedUsers[0] || this.task.group.completedBy.userId,
|
||||
});
|
||||
this.task.completed = false;
|
||||
return;
|
||||
}
|
||||
if (this.isYesterdaily === true) {
|
||||
await this.beforeTaskScore(this.task);
|
||||
this.task.completed = !this.task.completed;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
size="sm"
|
||||
:hide-footer="true"
|
||||
@hidden="onClose()"
|
||||
@show="handleOpen()"
|
||||
@show="syncTask()"
|
||||
@shown="focusInput()"
|
||||
>
|
||||
<div
|
||||
@@ -22,7 +22,9 @@
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div class="ml-auto d-flex align-items-center">
|
||||
<div
|
||||
class="ml-auto d-flex align-items-center"
|
||||
>
|
||||
<button
|
||||
class="cancel-task-btn mr-3"
|
||||
:class="cssClass('headings')"
|
||||
@@ -55,7 +57,7 @@
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="`${$t('text')}*`"
|
||||
/>
|
||||
<input
|
||||
@@ -66,28 +68,29 @@
|
||||
type="text"
|
||||
required="required"
|
||||
spellcheck="true"
|
||||
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:disabled="challengeAccessRequired"
|
||||
:placeholder="$t('addATitle')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUserTask || isChallengeTask || isOriginalChallengeTask"
|
||||
class="form-group mb-0"
|
||||
>
|
||||
<label
|
||||
class="d-flex align-items-center justify-content-between mb-1"
|
||||
>
|
||||
<span
|
||||
:class="cssClass('headings')"
|
||||
>{{ $t('notes') }}</span>
|
||||
<small>
|
||||
<div class="d-flex">
|
||||
<lockable-label
|
||||
class="mr-auto"
|
||||
:class-override="cssClass('headings')"
|
||||
:text="`${$t('notes')}`"
|
||||
/>
|
||||
<small
|
||||
class="my-1"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet"
|
||||
:class="cssClass('headings')"
|
||||
>{{ $t('markdownHelpLink') }}</a>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="task.notes"
|
||||
class="form-control input-notes"
|
||||
@@ -95,49 +98,6 @@
|
||||
:placeholder="$t('addNotes')"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="showManagerNotes"
|
||||
class="form-group mb-0 mt-3"
|
||||
>
|
||||
<lockable-label
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="groupAccessRequiredAndOnPersonalPage"
|
||||
:text="$t('managerNotes')"
|
||||
/>
|
||||
<textarea
|
||||
v-model="managerNotes"
|
||||
class="form-control input-notes"
|
||||
:class="cssClass('input')"
|
||||
:placeholder="$t('addNotes')"
|
||||
:disabled="groupAccessRequiredAndOnPersonalPage"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.group && task.group.assignedDate && !task.group.assigningUsername"
|
||||
class="mt-3 mb-n2"
|
||||
:class="cssClass('headings')"
|
||||
v-html="$t('assignedDateOnly', {
|
||||
date: formattedDate(task.group.assignedDate),
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.group && task.group.assignedDate && task.group.assigningUsername"
|
||||
class="mt-3 mb-n2"
|
||||
:class="cssClass('headings')"
|
||||
v-html="$t('assignedDateAndUser', {
|
||||
username: task.group.assigningUsername,
|
||||
date: formattedDate(task.group.assignedDate),
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task && groupAccessRequiredAndOnPersonalPage
|
||||
&& (task.type === 'daily' || task.type === 'todo')"
|
||||
class="summary-sentence py-3 px-4"
|
||||
v-html="summarySentence"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="task"
|
||||
@@ -181,8 +141,6 @@
|
||||
>
|
||||
<checklist
|
||||
:items.sync="task.checklist"
|
||||
:disable-items="groupAccessRequiredAndOnPersonalPage"
|
||||
:disable-drag="groupAccessRequiredAndOnPersonalPage"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -194,7 +152,7 @@
|
||||
class="habit-option-container no-transition
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
:class="!task.up ? cssClass('habit-control-disabled') : ''"
|
||||
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:disabled="challengeAccessRequired"
|
||||
@click="toggleUpDirection()"
|
||||
>
|
||||
<div
|
||||
@@ -220,7 +178,7 @@
|
||||
class="habit-option-container no-transition
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
:class="!task.down ? cssClass('habit-control-disabled') : ''"
|
||||
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:disabled="challengeAccessRequired"
|
||||
@click="toggleDownDirection()"
|
||||
>
|
||||
<div
|
||||
@@ -243,11 +201,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<template
|
||||
v-if="task.type !== 'reward' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type !== 'reward'"
|
||||
>
|
||||
<div class="d-flex mt-3">
|
||||
<lockable-label
|
||||
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="$t('difficulty')"
|
||||
/>
|
||||
<div
|
||||
@@ -258,13 +216,12 @@
|
||||
</div>
|
||||
<select-difficulty
|
||||
:value="task.priority"
|
||||
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:disabled="challengeAccessRequired"
|
||||
@select="setDifficulty($event)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-if="task.type === 'todo' && !groupAccessRequiredAndOnPersonalPage
|
||||
&& (!challengeAccessRequired || task.date)"
|
||||
v-if="task.type === 'todo' && (!challengeAccessRequired || task.date)"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -281,7 +238,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -297,7 +254,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -314,7 +271,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -344,8 +301,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && task.frequency === 'weekly'
|
||||
&& !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily' && task.frequency === 'weekly'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -405,7 +361,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUserTask"
|
||||
v-if="!groupId"
|
||||
class="tags-select option mt-3"
|
||||
>
|
||||
<div class="tags-inline form-group row">
|
||||
@@ -428,16 +384,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'habit'"
|
||||
v-if="task.type === 'habit' && !groupId"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
:locked="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="$t('resetCounter')"
|
||||
/>
|
||||
<select-translated-array
|
||||
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:disabled="challengeAccessRequired"
|
||||
:items="['daily', 'weekly', 'monthly']"
|
||||
:value="task.frequency"
|
||||
@select="task.frequency = $event"
|
||||
@@ -448,25 +404,18 @@
|
||||
v-if="groupId"
|
||||
class="option group-options mt-3"
|
||||
>
|
||||
<div
|
||||
v-if="task.type === 'todo'"
|
||||
class="form-group"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
class="mb-1"
|
||||
>{{ $t('sharedCompletion') }}</label>
|
||||
<select-translated-array
|
||||
:items="['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']"
|
||||
:value="sharedCompletion"
|
||||
@select="sharedCompletion = $event"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group row mt-3 mb-3">
|
||||
<label
|
||||
v-once
|
||||
class="col-12 mb-1"
|
||||
>{{ $t('assignedTo') }}</label>
|
||||
class="col-10 mb-1"
|
||||
>{{ $t('assignTo') }}</label>
|
||||
<a
|
||||
v-if="assignedMembers.length > 0"
|
||||
class="col-2 text-right mt-1"
|
||||
@click="clearAssignments"
|
||||
>
|
||||
{{ $t('clear') }}
|
||||
</a>
|
||||
<div class="col-12">
|
||||
<select-multi
|
||||
ref="assignMembers"
|
||||
@@ -479,17 +428,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group flex-group mt-3 mb-4">
|
||||
<label
|
||||
v-once
|
||||
class="mb-0 flex"
|
||||
>{{ $t('approvalRequired') }}</label>
|
||||
<toggle-switch
|
||||
class="d-inline-block"
|
||||
:checked="requiresApproval"
|
||||
@change="updateRequiresApproval"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="advancedSettingsAvailable"
|
||||
@@ -605,9 +543,7 @@
|
||||
</b-collapse>
|
||||
</div>
|
||||
<div
|
||||
v-if="purpose !== 'create'
|
||||
&& !challengeAccessRequired
|
||||
&& !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="purpose !== 'create' && !challengeAccessRequired"
|
||||
class="d-flex justify-content-center mt-4 mb-4"
|
||||
>
|
||||
<button
|
||||
@@ -654,6 +590,12 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#task-modal {
|
||||
a:not(.dropdown-item) {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.modal-dialog.modal-sm {
|
||||
max-width: 448px;
|
||||
}
|
||||
@@ -686,9 +628,6 @@
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
&:not(:host-context(.tags-popup)) {
|
||||
border: none;
|
||||
}
|
||||
transition-property: border-color, box-shadow, color, background;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
&:focus:not(:disabled), &:active:not(:disabled), &:hover:not(:disabled) {
|
||||
@@ -1026,11 +965,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.summary-sentence {
|
||||
background-color: $gray-700;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
@@ -1051,12 +985,8 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import clone from 'lodash/clone';
|
||||
import keys from 'lodash/keys';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import moment from 'moment';
|
||||
import Datepicker from '@/components/ui/datepicker';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
import toggleCheckbox from '@/components/ui/toggleCheckbox';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { mapGetters, mapActions, mapState } from '@/libs/store';
|
||||
@@ -1066,6 +996,8 @@ import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty
|
||||
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
|
||||
import syncTask from '../../mixins/syncTask';
|
||||
|
||||
import informationIcon from '@/assets/svg/information.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import negativeIcon from '@/assets/svg/negative.svg';
|
||||
@@ -1076,11 +1008,11 @@ import chevronIcon from '@/assets/svg/chevron.svg';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
||||
import gripIcon from '@/assets/svg/grip.svg';
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SelectMulti,
|
||||
Datepicker,
|
||||
toggleSwitch,
|
||||
checklist,
|
||||
selectDifficulty,
|
||||
selectTranslatedArray,
|
||||
@@ -1090,6 +1022,7 @@ export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [syncTask],
|
||||
// purpose is either create or edit, task is the task created or edited
|
||||
props: ['task', 'purpose', 'challengeId', 'groupId'],
|
||||
data () {
|
||||
@@ -1107,9 +1040,6 @@ export default {
|
||||
calendar: calendarIcon,
|
||||
grip: gripIcon,
|
||||
}),
|
||||
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
|
||||
sharedCompletion: 'singleCompletion',
|
||||
managerNotes: '',
|
||||
members: [],
|
||||
membersNameAndId: [],
|
||||
memberNamesById: {},
|
||||
@@ -1123,15 +1053,6 @@ export default {
|
||||
per: 'perception',
|
||||
},
|
||||
calendarHighlights: { dates: [new Date()] },
|
||||
expandDayString: {
|
||||
su: 'Sunday',
|
||||
m: 'Monday',
|
||||
t: 'Tuesday',
|
||||
w: 'Wednesday',
|
||||
th: 'Thursday',
|
||||
f: 'Friday',
|
||||
s: 'Saturday',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1150,7 +1071,6 @@ export default {
|
||||
|| this.task.type === 'todo'
|
||||
|| this.purpose === 'create'
|
||||
|| !this.isUserTask
|
||||
|| this.groupAccessRequiredAndOnPersonalPage
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -1164,29 +1084,17 @@ export default {
|
||||
|
||||
return true;
|
||||
},
|
||||
groupAccessRequiredAndOnPersonalPage () {
|
||||
if (!this.groupId && this.task.group && this.task.group.id) return true;
|
||||
return false;
|
||||
},
|
||||
checklistEnabled () {
|
||||
return ['daily', 'todo'].indexOf(this.task.type) > -1
|
||||
&& !this.isOriginalChallengeTask
|
||||
&& (!this.groupAccessRequiredAndOnPersonalPage || this.checklist.length > 0);
|
||||
},
|
||||
showManagerNotes () {
|
||||
return Boolean(this.task.group && this.task.group.managerNotes)
|
||||
|| (
|
||||
!this.groupAccessRequiredAndOnPersonalPage && this.managers.indexOf(this.user._id) !== -1
|
||||
);
|
||||
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
|
||||
},
|
||||
isChallengeTask () {
|
||||
return Boolean(this.task.challenge && this.task.challenge.id);
|
||||
},
|
||||
onUserPage () {
|
||||
isUserTask () {
|
||||
return !this.challengeId && !this.groupId;
|
||||
},
|
||||
challengeAccessRequired () {
|
||||
return this.onUserPage && this.isChallengeTask;
|
||||
return this.isUserTask && this.isChallengeTask;
|
||||
},
|
||||
isOriginalChallengeTask () {
|
||||
const isUserChallenge = Boolean(this.task.userId);
|
||||
@@ -1203,9 +1111,6 @@ export default {
|
||||
const type = this.$t(this.task.type);
|
||||
return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', { type });
|
||||
},
|
||||
isUserTask () {
|
||||
return !this.challengeId && !this.groupId;
|
||||
},
|
||||
repeatSuffix () {
|
||||
const { task } = this;
|
||||
|
||||
@@ -1240,23 +1145,6 @@ export default {
|
||||
selectedTags () {
|
||||
return this.getTagsFor(this.task);
|
||||
},
|
||||
summarySentence () {
|
||||
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)}
|
||||
task that will repeat
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
|
||||
}
|
||||
if (this.task.type === 'daily') {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)}
|
||||
task that repeats
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
|
||||
}
|
||||
if (this.task.date) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
|
||||
}
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
task () {
|
||||
@@ -1284,47 +1172,6 @@ export default {
|
||||
createTask: 'tasks:create',
|
||||
createTag: 'tags:createTag',
|
||||
}),
|
||||
async syncTask () {
|
||||
if (this.task?.group?.managerNotes) {
|
||||
this.managerNotes = this.task.group.managerNotes;
|
||||
}
|
||||
if (this.groupId && this.task.group?.approval) {
|
||||
this.requiresApproval = this.task.group.approval.required;
|
||||
}
|
||||
if (this.task?.group?.sharedCompletion) {
|
||||
this.sharedCompletion = this.task.group.sharedCompletion;
|
||||
} else if (this.task.group) {
|
||||
this.sharedCompletion = 'singleCompletion';
|
||||
}
|
||||
|
||||
if (this.groupId) {
|
||||
const members = await this.$store.dispatch('members:getGroupMembers', {
|
||||
groupId: this.groupId,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
this.members = members;
|
||||
this.membersNameAndId = [];
|
||||
this.members.forEach(member => {
|
||||
this.membersNameAndId.push({
|
||||
id: member._id,
|
||||
name: member.profile.name,
|
||||
addlText: `@${member.auth.local.username}`,
|
||||
});
|
||||
this.memberNamesById[member._id] = member.profile.name;
|
||||
});
|
||||
this.assignedMembers = [];
|
||||
if (this.task.group && this.task.group.assignedUsers) {
|
||||
this.assignedMembers = this.task.group.assignedUsers;
|
||||
}
|
||||
}
|
||||
|
||||
// @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);
|
||||
},
|
||||
async handleOpen () {
|
||||
this.syncTask();
|
||||
},
|
||||
cssClass (suffix) {
|
||||
if (!this.task) {
|
||||
return '';
|
||||
@@ -1350,104 +1197,6 @@ export default {
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
},
|
||||
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
|
||||
let activeDays;
|
||||
const dayStringArray = [];
|
||||
switch (frequency) {
|
||||
case 'weekly':
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
if (activeDays.length === 0) return ' on <strong>no days</strong>';
|
||||
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
|
||||
dayStringArray.push(' on <strong>');
|
||||
activeDays.forEach((value, index) => {
|
||||
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(` ${this.expandDayString[value]}`);
|
||||
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
break;
|
||||
case 'monthly':
|
||||
dayStringArray.push(' on <strong>the ');
|
||||
if (daysOfMonth.length > 0) {
|
||||
daysOfMonth.forEach((value, index) => {
|
||||
const stringDay = String(value);
|
||||
const stringFinalDigit = stringDay.slice(-1);
|
||||
let ordinalSuffix = 'th';
|
||||
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
|
||||
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
|
||||
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
|
||||
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
|
||||
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
} else if (weeksOfMonth.length > 0) {
|
||||
switch (weeksOfMonth[0]) {
|
||||
case 0:
|
||||
dayStringArray.push('first');
|
||||
break;
|
||||
case 1:
|
||||
dayStringArray.push('second');
|
||||
break;
|
||||
case 2:
|
||||
dayStringArray.push('third');
|
||||
break;
|
||||
case 3:
|
||||
dayStringArray.push('fourth');
|
||||
break;
|
||||
case 4:
|
||||
dayStringArray.push('fifth');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
|
||||
}
|
||||
break;
|
||||
case 'yearly':
|
||||
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
return dayStringArray.join('');
|
||||
},
|
||||
formattedDifficulty (priority) {
|
||||
switch (priority) {
|
||||
case 0.1:
|
||||
return 'a <strong>trivial</strong>';
|
||||
case 1:
|
||||
return 'an <strong>easy</strong>';
|
||||
case 1.5:
|
||||
return 'a <strong>medium</strong>';
|
||||
case 2:
|
||||
return 'a <strong>hard</strong>';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
formattedRepeatInterval (frequency, everyX) {
|
||||
const numericX = Number(everyX);
|
||||
switch (frequency) {
|
||||
case 'daily':
|
||||
if (numericX === 1) return '<strong>every day</strong>';
|
||||
if (numericX === 2) return '<strong>every other day</strong>';
|
||||
return `<strong>every ${numericX} days</strong>`;
|
||||
case 'weekly':
|
||||
if (numericX === 1) return '<strong>every week</strong>';
|
||||
if (numericX === 2) return '<strong>every other week</strong>';
|
||||
return `<strong>every ${numericX} weeks</strong>`;
|
||||
case 'monthly':
|
||||
if (numericX === 1) return '<strong>every month</strong>';
|
||||
if (numericX === 2) return '<strong>every other month</strong>';
|
||||
return `<strong>every ${numericX} months</strong>`;
|
||||
case 'yearly':
|
||||
if (numericX === 1) return '<strong>every year</strong>';
|
||||
return `<strong>every ${everyX} years</strong>`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
calculateMonthlyRepeatDays (newRepeatsOn) {
|
||||
if (!this.task) return;
|
||||
const { task } = this;
|
||||
@@ -1475,16 +1224,6 @@ export default {
|
||||
if (!this.canSave) return;
|
||||
if (this.newChecklistItem) this.addChecklistItem();
|
||||
|
||||
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
|
||||
if (this.groupId) {
|
||||
this.task.requiresApproval = this.requiresApproval;
|
||||
this.task.group.approval.required = this.requiresApproval;
|
||||
this.task.sharedCompletion = this.sharedCompletion;
|
||||
this.task.group.sharedCompletion = this.sharedCompletion;
|
||||
this.task.managerNotes = this.managerNotes;
|
||||
this.task.group.managerNotes = this.managerNotes;
|
||||
}
|
||||
|
||||
if (this.task.type === 'reward' && this.task.value === '') {
|
||||
this.task.value = 0;
|
||||
}
|
||||
@@ -1503,12 +1242,18 @@ export default {
|
||||
tasks: [this.task],
|
||||
});
|
||||
Object.assign(this.task, response);
|
||||
const promises = this.assignedMembers.map(memberId => this.$store.dispatch('tasks:assignTask', {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: memberId,
|
||||
}));
|
||||
Promise.all(promises);
|
||||
this.task.group.assignedUsers = this.assignedMembers;
|
||||
assignedUserIds: this.assignedMembers,
|
||||
});
|
||||
this.assignedMembers.forEach(memberId => {
|
||||
if (!this.task.assignedUsersDetail) this.task.assignedUsersDetail = {};
|
||||
this.task.assignedUsersDetail[memberId] = {
|
||||
assignedDate: new Date(),
|
||||
assigningUsername: this.user.auth.local.username,
|
||||
completed: false,
|
||||
};
|
||||
});
|
||||
this.$emit('taskCreated', this.task);
|
||||
} else {
|
||||
this.createTask(this.task);
|
||||
@@ -1530,22 +1275,14 @@ export default {
|
||||
this.$root.$emit('bv::hide::modal', 'task-modal');
|
||||
},
|
||||
onClose () {
|
||||
if (this.task.group && this.task.group.managerNotes) this.managerNotes = null;
|
||||
this.newChecklistItem = '';
|
||||
this.$emit('cancel');
|
||||
},
|
||||
updateRequiresApproval (newValue) {
|
||||
let truthy = true;
|
||||
if (!newValue) truthy = false; // This return undefined instad of false
|
||||
this.requiresApproval = truthy;
|
||||
},
|
||||
async toggleAssignment (memberId) {
|
||||
if (this.purpose === 'create') {
|
||||
return;
|
||||
}
|
||||
|
||||
const assignedIndex = this.assignedMembers.indexOf(memberId);
|
||||
|
||||
if (assignedIndex === -1) {
|
||||
await this.$store.dispatch('tasks:unassignTask', {
|
||||
taskId: this.task._id,
|
||||
@@ -1554,10 +1291,21 @@ export default {
|
||||
} else {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: memberId,
|
||||
assignedUserIds: [memberId],
|
||||
});
|
||||
}
|
||||
},
|
||||
async clearAssignments () {
|
||||
if (this.purpose === 'edit') {
|
||||
for (const assignedMember of this.assignedMembers) {
|
||||
await this.$store.dispatch('tasks:unassignTask', { // eslint-disable-line no-await-in-loop
|
||||
taskId: this.task._id,
|
||||
userId: assignedMember,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.assignedMembers = [];
|
||||
},
|
||||
focusInput () {
|
||||
this.$refs.inputToFocus.focus();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="task-summary"
|
||||
:hide-footer="true"
|
||||
@hidden="$emit('cancel')"
|
||||
>
|
||||
<div
|
||||
v-if="task"
|
||||
slot="modal-header"
|
||||
class="task-modal-header px-4 d-flex align-items-center"
|
||||
:class="cssClass('bg')"
|
||||
>
|
||||
<h2
|
||||
class="my-auto"
|
||||
:class="cssClass('headings')"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div
|
||||
class="svg-icon color close-x ml-auto my-auto"
|
||||
:class="cssClass('headings')"
|
||||
aria-hidden="true"
|
||||
tabindex="0"
|
||||
@click="cancel()"
|
||||
@keypress.enter="cancel()"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task"
|
||||
class="task-modal-content pt-3 px-4 pb-4"
|
||||
>
|
||||
<div class="summary-block">
|
||||
<h3> {{ $t('title') }} </h3>
|
||||
<p> {{ task.text }} </p>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.notes"
|
||||
class="summary-block"
|
||||
>
|
||||
<h3> {{ $t('notes') }} </h3>
|
||||
<p> {{ task.notes }} </p>
|
||||
</div>
|
||||
<div class="summary-block">
|
||||
<h3> {{ $t('description') }} </h3>
|
||||
<p v-html="summarySentence" ></p>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.checklist && task.checklist.length > 0"
|
||||
class="summary-block"
|
||||
>
|
||||
<checklist
|
||||
:items.sync="task.checklist"
|
||||
:disableDrag="true"
|
||||
:disableEdit="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="summary-block"
|
||||
v-if="assignedUsernames.length > 0"
|
||||
>
|
||||
<h3> {{ $t('assignedTo') }} </h3>
|
||||
<div
|
||||
class="d-flex flex-wrap"
|
||||
>
|
||||
<span
|
||||
v-for="member in assignedUsernames"
|
||||
:key="member"
|
||||
class="assigned-member py-1 px-75 mb-1 mr-1"
|
||||
>
|
||||
@{{ member }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task && task.group && task.group.assignedUsersDetail
|
||||
&& task.group.assignedUsersDetail[user._id]"
|
||||
class="assignment-footer text-center py-2"
|
||||
v-html="$t('assignedDateAndUser', {
|
||||
username: task.group.assignedUsersDetail[user._id].assigningUsername,
|
||||
date: formattedDate(task.group.assignedUsersDetail[user._id].assignedDate),
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
#task-summary {
|
||||
overflow-y: hidden;
|
||||
|
||||
.modal-content {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
|
||||
}
|
||||
|
||||
.modal-header, .modal-body, .modal-footer {
|
||||
padding: 0px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
margin-top: 50vh;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.assigned-member {
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 100px;
|
||||
color: $gray-100;
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.assignment-footer {
|
||||
color: $gray-100;
|
||||
background-color: $gray-700;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.close-x {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-block:not(:last-of-type) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.task-modal-content {
|
||||
h3 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3, p {
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.task-modal-header {
|
||||
color: $white;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
|
||||
h2 {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import keys from 'lodash/keys';
|
||||
import moment from 'moment';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
|
||||
import checklist from './modal-controls/checklist';
|
||||
import { mapGetters, mapState } from '@/libs/store';
|
||||
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
checklist,
|
||||
},
|
||||
props: ['task'],
|
||||
data () {
|
||||
return {
|
||||
expandDayString: {
|
||||
su: 'Sunday',
|
||||
m: 'Monday',
|
||||
t: 'Tuesday',
|
||||
w: 'Wednesday',
|
||||
th: 'Thursday',
|
||||
f: 'Friday',
|
||||
s: 'Saturday',
|
||||
},
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getTaskClasses: 'tasks:getTaskClasses',
|
||||
}),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
assignedUsernames () {
|
||||
if (!this.task.group || !this.task.group.assignedUsers
|
||||
|| !this.task.group.assignedUsersDetail) return [];
|
||||
const usernames = [];
|
||||
for (const user of this.task.group.assignedUsers) {
|
||||
usernames.push(this.task.group.assignedUsersDetail[user].assignedUsername);
|
||||
}
|
||||
return usernames;
|
||||
},
|
||||
summarySentence () {
|
||||
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
|
||||
}
|
||||
if (this.task.type === 'daily') {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
|
||||
}
|
||||
if (this.task.date) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
|
||||
}
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
|
||||
},
|
||||
title () {
|
||||
const type = this.$t(this.task.type);
|
||||
return this.$t('taskSummary', { type });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancel () {
|
||||
this.$root.$emit('bv::hide::modal', 'task-summary');
|
||||
},
|
||||
cssClass (suffix) {
|
||||
if (!this.task) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.getTaskClasses(this.task, `edit-modal-${suffix}`);
|
||||
},
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
},
|
||||
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
|
||||
let activeDays;
|
||||
const dayStringArray = [];
|
||||
switch (frequency) {
|
||||
case 'weekly':
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
if (activeDays.length === 0) return ' on <strong>no days</strong>';
|
||||
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
|
||||
dayStringArray.push(' on <strong>');
|
||||
activeDays.forEach((value, index) => {
|
||||
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(` ${this.expandDayString[value]}`);
|
||||
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
break;
|
||||
case 'monthly':
|
||||
dayStringArray.push(' on <strong>the ');
|
||||
if (daysOfMonth.length > 0) {
|
||||
daysOfMonth.forEach((value, index) => {
|
||||
const stringDay = String(value);
|
||||
const stringFinalDigit = stringDay.slice(-1);
|
||||
let ordinalSuffix = 'th';
|
||||
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
|
||||
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
|
||||
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
|
||||
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
|
||||
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
} else if (weeksOfMonth.length > 0) {
|
||||
switch (weeksOfMonth[0]) {
|
||||
case 0:
|
||||
dayStringArray.push('first');
|
||||
break;
|
||||
case 1:
|
||||
dayStringArray.push('second');
|
||||
break;
|
||||
case 2:
|
||||
dayStringArray.push('third');
|
||||
break;
|
||||
case 3:
|
||||
dayStringArray.push('fourth');
|
||||
break;
|
||||
case 4:
|
||||
dayStringArray.push('fifth');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
|
||||
}
|
||||
break;
|
||||
case 'yearly':
|
||||
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
return dayStringArray.join('');
|
||||
},
|
||||
formattedDifficulty (priority) {
|
||||
switch (priority) {
|
||||
case 0.1:
|
||||
return 'a <strong>trivial</strong>';
|
||||
case 1:
|
||||
return 'an <strong>easy</strong>';
|
||||
case 1.5:
|
||||
return 'a <strong>medium</strong>';
|
||||
case 2:
|
||||
return 'a <strong>hard</strong>';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
formattedRepeatInterval (frequency, everyX) {
|
||||
const numericX = Number(everyX);
|
||||
switch (frequency) {
|
||||
case 'daily':
|
||||
if (numericX === 1) return '<strong>every day</strong>';
|
||||
if (numericX === 2) return '<strong>every other day</strong>';
|
||||
return `<strong>every ${numericX} days</strong>`;
|
||||
case 'weekly':
|
||||
if (numericX === 1) return '<strong>every week</strong>';
|
||||
if (numericX === 2) return '<strong>every other week</strong>';
|
||||
return `<strong>every ${numericX} weeks</strong>`;
|
||||
case 'monthly':
|
||||
if (numericX === 1) return '<strong>every month</strong>';
|
||||
if (numericX === 2) return '<strong>every other month</strong>';
|
||||
return `<strong>every ${numericX} months</strong>`;
|
||||
case 'yearly':
|
||||
if (numericX === 1) return '<strong>every year</strong>';
|
||||
return `<strong>every ${everyX} years</strong>`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="row user-tasks-page">
|
||||
<div
|
||||
class="row user-tasks-page"
|
||||
@click="openCreateBtn ? openCreateBtn = false : null"
|
||||
>
|
||||
<broken-task-modal />
|
||||
<task-modal
|
||||
ref="taskModal"
|
||||
@@ -7,6 +10,11 @@
|
||||
:purpose="creatingTask !== null ? 'create' : 'edit'"
|
||||
@cancel="cancelTaskModal()"
|
||||
/>
|
||||
<task-summary
|
||||
ref="taskSummary"
|
||||
:task="editingTask"
|
||||
@cancel="cancelTaskModal()"
|
||||
/>
|
||||
<div class="col-12">
|
||||
<div class="row tasks-navigation">
|
||||
<div class="col-12 col-md-4 offset-md-4">
|
||||
@@ -160,45 +168,43 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="create-task-area d-flex">
|
||||
<transition name="slide-tasks-btns">
|
||||
<div class="create-task-area">
|
||||
<div
|
||||
id="create-task-btn"
|
||||
class="btn btn-primary create-btn d-flex align-items-center"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click.stop.prevent="openCreateBtn = !openCreateBtn"
|
||||
@keypress.enter="openCreateBtn = !openCreateBtn"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
v-if="openCreateBtn"
|
||||
class="d-flex"
|
||||
class="svg-icon icon-10 color"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
|
||||
</div>
|
||||
<div
|
||||
v-if="openCreateBtn"
|
||||
class="dropdown"
|
||||
>
|
||||
<div
|
||||
v-for="type in columns"
|
||||
:key="type"
|
||||
@click="createTask(type)"
|
||||
class="dropdown-item d-flex px-2 py-1"
|
||||
>
|
||||
<div
|
||||
v-for="type in columns"
|
||||
:key="type"
|
||||
v-b-tooltip.hover.bottom="$t(type)"
|
||||
class="create-task-btn diamond-btn"
|
||||
@click="createTask(type)"
|
||||
>
|
||||
<div class="d-flex align-items-center justify-content-center task-icon">
|
||||
<div
|
||||
class="svg-icon"
|
||||
class="svg-icon m-auto"
|
||||
:class="`icon-${type}`"
|
||||
v-html="icons[type]"
|
||||
></div>
|
||||
</div>
|
||||
<div class="task-label ml-2">
|
||||
{{ $t(type) }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div
|
||||
id="create-task-btn"
|
||||
class="create-btn diamond-btn btn btn-success"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click="openCreateBtn = !openCreateBtn"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
</div>
|
||||
<b-tooltip
|
||||
v-if="!openCreateBtn"
|
||||
target="create-task-btn"
|
||||
placement="bottom"
|
||||
>
|
||||
{{ $t('addTask') }}
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row tasks-columns">
|
||||
@@ -211,6 +217,7 @@
|
||||
:search-text="searchTextThrottled"
|
||||
:selected-tags="selectedTags"
|
||||
@editTask="editTask"
|
||||
@taskSummary="taskSummary"
|
||||
@openBuyDialog="openBuyDialog($event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -345,7 +352,7 @@
|
||||
}
|
||||
|
||||
.create-task-area {
|
||||
top: -2.5rem;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.drag {
|
||||
@@ -381,6 +388,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import draggable from 'vuedraggable';
|
||||
import TaskColumn from './column';
|
||||
import TaskModal from './taskModal';
|
||||
import TaskSummary from './taskSummary';
|
||||
import spells from './spells';
|
||||
import markdown from '@/directives/markdown';
|
||||
|
||||
@@ -401,6 +409,7 @@ export default {
|
||||
components: {
|
||||
TaskColumn,
|
||||
TaskModal,
|
||||
TaskSummary,
|
||||
spells,
|
||||
brokenTaskModal,
|
||||
draggable,
|
||||
@@ -524,13 +533,19 @@ export default {
|
||||
};
|
||||
this.newTag = null;
|
||||
},
|
||||
// Need Vue.nextTick() otherwise the first time the modal is not rendered
|
||||
editTask (task) {
|
||||
this.editingTask = cloneDeep(task);
|
||||
// Necessary otherwise the first time the modal is not rendered
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
taskSummary (task) {
|
||||
this.editingTask = cloneDeep(task);
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-summary');
|
||||
});
|
||||
},
|
||||
createTask (type) {
|
||||
this.openCreateBtn = false;
|
||||
this.creatingTask = taskDefaults({ type, text: '' }, this.user);
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import scoreTask from '@/mixins/scoreTask';
|
||||
import sync from '@/mixins/sync';
|
||||
import Task from './task';
|
||||
import LoadingSpinner from '../ui/loadingSpinner';
|
||||
|
||||
@@ -92,7 +93,7 @@ export default {
|
||||
Task,
|
||||
LoadingSpinner,
|
||||
},
|
||||
mixins: [scoreTask],
|
||||
mixins: [scoreTask, sync],
|
||||
props: {
|
||||
yesterDailies: {
|
||||
type: Array,
|
||||
@@ -180,6 +181,7 @@ export default {
|
||||
|
||||
this.isLoading = false;
|
||||
this.$root.$emit('bv::hide::modal', 'yesterdaily');
|
||||
if (this.$route.fullPath.indexOf('task-information') !== -1) this.sync();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<template v-slot:button-content>
|
||||
<slot
|
||||
name="item"
|
||||
:item="selected"
|
||||
:item="selected || placeholder"
|
||||
:button="true"
|
||||
>
|
||||
<!-- Fallback content -->
|
||||
@@ -114,6 +114,9 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -29,6 +29,11 @@ export default [
|
||||
type: 'Staff',
|
||||
uuid: '61b2c855-0a30-444c-bcc6-1cac876460b0',
|
||||
},
|
||||
{
|
||||
name: 'heyeilatan',
|
||||
type: 'Staff',
|
||||
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
|
||||
},
|
||||
{
|
||||
name: 'Alys',
|
||||
type: 'Moderator',
|
||||
|
||||
@@ -17,8 +17,14 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
hideSet (set) {
|
||||
return moment(appearanceSets[set.key].availableUntil).isBefore(moment());
|
||||
hideSet (setKey) {
|
||||
if (appearanceSets[setKey].availableFrom) {
|
||||
return !moment().isBetween(
|
||||
appearanceSets[setKey].availableFrom,
|
||||
appearanceSets[setKey].availableUntil,
|
||||
);
|
||||
}
|
||||
return moment(appearanceSets[setKey].availableUntil).isBefore(moment());
|
||||
},
|
||||
mapKeysToFreeOption (key, type, subType) {
|
||||
const userPreference = subType
|
||||
@@ -43,8 +49,8 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
|
||||
const locked = !userPurchased || !userPurchased[key];
|
||||
let hide = false;
|
||||
|
||||
if (set && appearanceSets[set]) {
|
||||
if (locked) hide = moment(appearanceSets[set].availableUntil).isBefore(moment());
|
||||
if (set && appearanceSets[set] && locked) {
|
||||
hide = this.hideSet(set);
|
||||
}
|
||||
|
||||
option.gemLocked = locked;
|
||||
|
||||