Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24430861ce | ||
|
|
e60285e7d9 | ||
|
|
f030135c82 | ||
|
|
2c29310466 | ||
|
|
13c0d12045 | ||
|
|
6456984f57 | ||
|
|
cfc1a12930 | ||
|
|
9ba4687478 | ||
|
|
6d987a9579 | ||
|
|
4702479156 | ||
|
|
b384cd4eb8 | ||
|
|
d2bd7dc325 | ||
|
|
24841346dc | ||
|
|
10f5011781 | ||
|
|
9a896470d5 | ||
|
|
6b0b393e32 | ||
|
|
5f440f1bfa | ||
|
|
d4f9555f11 | ||
|
|
203d97423a | ||
|
|
0f4711c358 | ||
|
|
38bf0b3721 | ||
|
|
88c8b545f4 | ||
|
|
184ee7262e | ||
|
|
6df4ce251c | ||
|
|
e383614107 | ||
|
|
2b21410abd | ||
|
|
56f956be5a | ||
|
|
2b44d32b1c | ||
|
|
11347e5679 | ||
|
|
a04479e689 | ||
|
|
4ce4e55e80 | ||
|
|
755f51b674 | ||
|
|
6c1b21117f | ||
|
|
14441701c9 | ||
|
|
ccb821fd6f | ||
|
|
3664a1ebb1 | ||
|
|
509cb00374 | ||
|
|
bc4770577a | ||
|
|
f158852be5 | ||
|
|
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 | ||
|
|
71fa4d6cb7 | ||
|
|
922b2e985a | ||
|
|
b82239811c | ||
|
|
f0b5637e9e | ||
|
|
2c93b3e2e3 | ||
|
|
72a9417de9 | ||
|
|
1701fc702b | ||
|
|
16dc6a1b4c | ||
|
|
d3a91aab72 | ||
|
|
0528ee1761 | ||
|
|
1b4d670b0a | ||
|
|
3654e01fee | ||
|
|
fdbeda19e2 | ||
|
|
db723d79a4 | ||
|
|
d5d1bfbd99 | ||
|
|
d06f4f4e1e | ||
|
|
82c4260fca | ||
|
|
0f3a26a490 | ||
|
|
9c2963e557 | ||
|
|
d5c4e1666e | ||
|
|
79071e3445 | ||
|
|
2ea707c27c | ||
|
|
f7b727dc95 | ||
|
|
9b9503b141 | ||
|
|
a78aea5456 | ||
|
|
6e8e7318f3 | ||
|
|
1c3d4a6fd5 | ||
|
|
a418752041 | ||
|
|
c9b3c48379 | ||
|
|
e2c6fb1ea2 | ||
|
|
7f8e44ff49 | ||
|
|
e0e9381584 | ||
|
|
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",
|
||||
|
||||
71
migrations/tasks/team-tasks-v2.js
Normal file
@@ -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);
|
||||
}
|
||||
1875
package-lock.json
generated
14
package.json
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.237.1",
|
||||
"version": "4.244.0",
|
||||
"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"
|
||||
|
||||
105
scripts/team-cron.js
Normal file
@@ -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({
|
||||
|
||||
@@ -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.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
237
website/client/package-lock.json
generated
@@ -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;
|
||||
@@ -1434,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;
|
||||
@@ -1834,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;
|
||||
@@ -2121,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;
|
||||
@@ -2995,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;
|
||||
@@ -3400,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;
|
||||
@@ -18505,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;
|
||||
@@ -18515,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;
|
||||
@@ -19720,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;
|
||||
@@ -19730,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;
|
||||
@@ -22820,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;
|
||||
@@ -22990,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;
|
||||
@@ -23115,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;
|
||||
@@ -23270,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;
|
||||
@@ -23440,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;
|
||||
@@ -23565,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;
|
||||
@@ -23720,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;
|
||||
@@ -23880,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;
|
||||
@@ -24040,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;
|
||||
@@ -27010,6 +27235,31 @@
|
||||
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_301404 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
|
||||
width: 90px;
|
||||
@@ -36688,6 +36938,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;
|
||||
@@ -37103,6 +37358,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;
|
||||
@@ -37618,6 +37878,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;
|
||||
@@ -38033,6 +38298,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;
|
||||
@@ -38298,6 +38568,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;
|
||||
@@ -38953,6 +39228,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;
|
||||
@@ -39438,6 +39718,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;
|
||||
@@ -40658,6 +40943,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;
|
||||
@@ -41233,6 +41523,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;
|
||||
@@ -41753,6 +42048,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;
|
||||
@@ -42168,6 +42468,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;
|
||||
@@ -42683,6 +42988,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;
|
||||
@@ -43098,6 +43408,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;
|
||||
@@ -43363,6 +43678,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;
|
||||
@@ -44018,6 +44338,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;
|
||||
@@ -44503,6 +44828,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;
|
||||
@@ -45723,6 +46053,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;
|
||||
@@ -46298,6 +46633,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;
|
||||
@@ -46823,6 +47163,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;
|
||||
@@ -47238,6 +47583,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;
|
||||
@@ -47753,6 +48103,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;
|
||||
@@ -48168,6 +48523,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;
|
||||
@@ -48433,6 +48793,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;
|
||||
@@ -49093,6 +49458,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;
|
||||
@@ -49578,6 +49948,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;
|
||||
@@ -50798,6 +51173,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;
|
||||
@@ -51373,6 +51753,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;
|
||||
@@ -51903,6 +52288,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;
|
||||
@@ -52333,6 +52723,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;
|
||||
@@ -52868,6 +53263,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;
|
||||
@@ -53298,6 +53698,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;
|
||||
@@ -53578,6 +53983,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;
|
||||
@@ -54253,6 +54663,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;
|
||||
@@ -54753,6 +55168,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;
|
||||
@@ -55998,6 +56418,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;
|
||||
@@ -56593,6 +57018,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;
|
||||
@@ -56923,6 +57353,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;
|
||||
|
||||
BIN
website/client/src/assets/images/bug.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
website/client/src/assets/images/group-plans/purple-diagonal.png
Normal file
|
After Width: | Height: | Size: 410 B |
BIN
website/client/src/assets/images/group-plans/task-columns.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
website/client/src/assets/images/group-plans/task-columns@2x.png
Normal file
|
After Width: | Height: | Size: 741 B |
BIN
website/client/src/assets/images/group-plans/task-columns@3x.png
Normal file
|
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
3
website/client/src/assets/svg/last-complete.svg
Normal file
@@ -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 |
3
website/client/src/assets/svg/tumblr.svg
Normal file
@@ -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 |
@@ -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 () {
|
||||
|
||||
@@ -112,10 +112,8 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'group._id': {
|
||||
async groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
'group._id': function groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -92,7 +92,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)"
|
||||
@@ -241,7 +240,6 @@
|
||||
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)"
|
||||
@@ -266,6 +264,11 @@
|
||||
|
||||
.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 +282,8 @@
|
||||
|
||||
.payment-providers {
|
||||
width: 350px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +403,10 @@
|
||||
.payment-options {
|
||||
margin-bottom: 4em;
|
||||
|
||||
h4 {
|
||||
color: #34313a;
|
||||
}
|
||||
|
||||
.purple-box {
|
||||
background-color: #4f2a93;
|
||||
color: #fff;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 })"
|
||||
|
||||
@@ -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>
|
||||
32
website/client/src/components/tasks/spells.vue
Normal file → Executable file
@@ -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();
|
||||
},
|
||||
|
||||
358
website/client/src/components/tasks/taskSummary.vue
Normal file
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -15,24 +15,8 @@ export default {
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async beforeTaskScore (task) {
|
||||
const { user } = this;
|
||||
if (this.castingSpell) return false;
|
||||
|
||||
if (task.group.approval.required && !task.group.approval.approved) {
|
||||
task.group.approval.requested = true;
|
||||
const { data: groupPlans } = await this.$store.dispatch('guilds:getGroupPlans');
|
||||
const groupPlan = groupPlans.find(g => g.id === task.group.id);
|
||||
if (groupPlan) {
|
||||
const managers = Object.keys(groupPlan.managers);
|
||||
managers.push(groupPlan.leader);
|
||||
if (managers.indexOf(user._id) !== -1) {
|
||||
task.group.approval.approved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
async beforeTaskScore () {
|
||||
return (!this.castingSpell);
|
||||
},
|
||||
playTaskScoreSound (task, direction) {
|
||||
switch (task.type) { // eslint-disable-line default-case
|
||||
|
||||
32
website/client/src/mixins/syncTask.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import clone from 'lodash/clone';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
async syncTask () {
|
||||
if (this.groupId || this.task?.group.id) {
|
||||
const members = await this.$store.dispatch('members:getGroupMembers', {
|
||||
groupId: this.groupId || this.task?.group.id,
|
||||
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?.assignedUsers) {
|
||||
this.assignedMembers = this.task.group.assignedUsers;
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: Task modal 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);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -204,7 +204,7 @@ export async function createGroupTasks (store, payload) {
|
||||
}
|
||||
|
||||
export async function assignTask (store, payload) {
|
||||
const response = await axios.post(`/api/v4/tasks/${payload.taskId}/assign/${payload.userId}`);
|
||||
const response = await axios.post(`/api/v4/tasks/${payload.taskId}/assign`, payload.assignedUserIds);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,8 @@ export function canEdit (store) {
|
||||
const user = store.state.user.data;
|
||||
const userId = user.id || user._id;
|
||||
|
||||
const isUserAdmin = user.permissions && user.permissions.challengeAdmin;
|
||||
const isUserAdmin = user.permissions
|
||||
&& (user.permissions.challengeAdmin || user.permissions.fullAccess);
|
||||
const isUserGroupLeader = group && (group.leader
|
||||
&& group.leader._id === userId);
|
||||
const isUserGroupManager = group && (group.managers
|
||||
@@ -101,11 +102,7 @@ export function canEdit (store) {
|
||||
}
|
||||
break;
|
||||
case 'group':
|
||||
if (!onUserDashboard) {
|
||||
isUserCanEditTask = isUserGroupLeader || isUserGroupManager || isUserAdmin;
|
||||
} else {
|
||||
isUserCanEditTask = true;
|
||||
}
|
||||
isUserCanEditTask = isUserGroupLeader || isUserGroupManager || isUserAdmin;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -114,15 +111,20 @@ export function canEdit (store) {
|
||||
};
|
||||
}
|
||||
|
||||
function _nonInteractive (task) {
|
||||
return (task.group && task.group.id && !task.userId)
|
||||
|| (task.challenge && task.challenge.id && !task.userId)
|
||||
|| (task.group && task.group.approval && task.group.approval.requested
|
||||
&& task.type !== 'habit');
|
||||
function _nonInteractive (task, userId) {
|
||||
if (task.userId) return false;
|
||||
if (task.challenge && task.challenge.id) return true;
|
||||
if (
|
||||
task.group && task.group.assignedUsers
|
||||
&& task.group.assignedUsers.length > 0
|
||||
&& task.group.assignedUsers.indexOf(userId) === -1
|
||||
) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getTaskClasses (store) {
|
||||
const userPreferences = store.state.user.data.preferences;
|
||||
const userId = store.state.user.data._id;
|
||||
|
||||
// Purpose can be one of the following strings:
|
||||
// Edit Modal: edit-modal-bg, edit-modal-text, edit-modal-icon
|
||||
@@ -169,9 +171,14 @@ export function getTaskClasses (store) {
|
||||
|
||||
case 'control':
|
||||
if (type === 'todo' || type === 'daily') {
|
||||
if (task.completed || (!shouldDo(dueDate, task, userPreferences) && type === 'daily')) {
|
||||
if (task.completed
|
||||
|| (!shouldDo(dueDate, task, userPreferences) && type === 'daily')
|
||||
|| (task.group && task.group.assignedUsersDetail
|
||||
&& task.group.assignedUsersDetail[userId]
|
||||
&& task.group.assignedUsersDetail[userId].completed)
|
||||
) {
|
||||
return {
|
||||
bg: _nonInteractive(task) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg',
|
||||
bg: _nonInteractive(task, userId) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg',
|
||||
checkbox: 'task-disabled-daily-todo-control-checkbox',
|
||||
inner: 'task-disabled-daily-todo-control-inner',
|
||||
content: 'task-disabled-daily-todo-control-content',
|
||||
@@ -179,28 +186,28 @@ export function getTaskClasses (store) {
|
||||
}
|
||||
|
||||
return {
|
||||
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
checkbox: `task-${color}-control-checkbox`,
|
||||
inner: `task-${color}-control-inner-daily-todo`,
|
||||
icon: `task-${color}-control-icon`,
|
||||
};
|
||||
} if (type === 'reward') {
|
||||
return {
|
||||
bg: _nonInteractive(task) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg',
|
||||
bg: _nonInteractive(task, userId) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg',
|
||||
};
|
||||
} if (type === 'habit') {
|
||||
return {
|
||||
up: task.up
|
||||
? {
|
||||
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
icon: `task-${color}-control-icon`,
|
||||
}
|
||||
: { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
|
||||
down: task.down
|
||||
? {
|
||||
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
icon: `task-${color}-control-icon`,
|
||||
}
|
||||
: { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
|
||||
|
||||
@@ -35,7 +35,10 @@ describe('canEdit getter', () => {
|
||||
});
|
||||
it('can Edit task in own dashboard', () => {
|
||||
expect(store.getters['tasks:canEdit'](task, 'challenge', true, null, challenge)).to.equal(true);
|
||||
expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(true);
|
||||
});
|
||||
|
||||
it('cannot Edit group task in own dashboard', () => {
|
||||
expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(false);
|
||||
});
|
||||
|
||||
it('can Edit any challenge task if admin', () => {
|
||||
|
||||
@@ -143,7 +143,14 @@ describe('getTaskClasses getter', () => {
|
||||
});
|
||||
|
||||
it('returns noninteractive classes and padlock icons for group board tasks', () => {
|
||||
const task = { type: 'todo', value: 2, group: { id: 'group-id' } };
|
||||
const task = {
|
||||
type: 'todo',
|
||||
value: 2,
|
||||
group: {
|
||||
id: 'group-id',
|
||||
assignedUsers: ['not-me'],
|
||||
},
|
||||
};
|
||||
expect(getTaskClasses(task, 'control')).to.deep.equal({
|
||||
bg: 'task-good-control-bg-noninteractive',
|
||||
checkbox: 'task-good-control-checkbox',
|
||||
|
||||
@@ -23,7 +23,6 @@ const envVars = [
|
||||
'BASE_URL',
|
||||
'GA_ID',
|
||||
'STRIPE_PUB_KEY',
|
||||
'FACEBOOK_KEY',
|
||||
'GOOGLE_CLIENT_ID',
|
||||
'APPLE_AUTH_CLIENT_ID',
|
||||
'AMPLITUDE_KEY',
|
||||
|
||||
@@ -119,5 +119,21 @@
|
||||
"achievementDomesticatedText": "لقد فقس جميع الألوان القياسية للحيوانات الأليفة المستأنسة: النمس ، وخنزير غينيا ، والديك ، والخنزير الطائر ، والجرذ ، والأرنب ، والحصان ، والبقر!",
|
||||
"achievementDomesticated": "ا-يا-ا-يا-يو",
|
||||
"achievementBirdsOfAFeatherModalText": "تقوم بجمع كل الحيوانات الأليفة الطائرة!",
|
||||
"achievementZodiacZookeeperText": "لقد فقس جميع الألوان القياسية للحيوانات الأليفة في الأبراج: الجرذ ، البقرة ، الأرنب ، الأفعى ، الحصان ، الأغنام ، القرد ، الديك ، الذئب ، النمر ، الخنزير الطائر ، والتنين!"
|
||||
"achievementZodiacZookeeperText": "لقد فقس جميع الألوان القياسية للحيوانات الأليفة في الأبراج: الجرذ ، البقرة ، الأرنب ، الأفعى ، الحصان ، الأغنام ، القرد ، الديك ، الذئب ، النمر ، الخنزير الطائر ، والتنين!",
|
||||
"achievementGroupsBeta2022ModalText": "لقد ساعدت أنت ومجموعاتك Habitica من خلال الاختبار وتقديم التعليقات!",
|
||||
"achievementGroupsBeta2022": "اختبار تجريبي تفاعلي",
|
||||
"achievementGroupsBeta2022Text": "قدمت أنت ومجموعتك تعليقات لا تقدر بثمن لمساعدة Habitica في الاختبار.",
|
||||
"achievementReptacularRumble": "الدمدمة الزاحفة",
|
||||
"achievementReptacularRumbleModalText": "لقد جمعت كل الزواحف الأليفة!",
|
||||
"achievementReptacularRumbleText": "لقد فقس جميع الألوان القياسية للحيوانات الأليفة الزواحف: التمساح ، الزاحف المجنح ، الأفعى ، ترايسيراتوبس ، السلحفاة ، التيرانوصور ريكس ، وفيلوسيرابتور!",
|
||||
"achievementBirdsOfAFeather": "متشابهون",
|
||||
"achievementZodiacZookeeper": "حارس حديقة الحيوانات الفلكية",
|
||||
"achievementShadyCustomerText": "لقد جمع كل حيوانات الظل الأليفة.",
|
||||
"achievementShadyCustomerModalText": "لقد قمت بتجميع كل حيوانات الظل الأليفة!",
|
||||
"achievementZodiacZookeeperModalText": "لقد قمت بتجميع كل الحيوانات الفلكية الأليفة!",
|
||||
"achievementBirdsOfAFeatherText": "لقد فقس جميع الألوان القياسية للحيوانات الأليفة الطائرة: الخنزير الطائر ، البومة ، الببغاء ، الزاحف المجنح ، الجريفون ، فالكون ، الطاووس ، والديك!",
|
||||
"achievementShadeOfItAllModalText": "لقد قمت بترويض كل حيوانات الظل للركوب!",
|
||||
"achievementShadyCustomer": "زبون الظل",
|
||||
"achievementShadeOfItAll": "ظل كل شيء",
|
||||
"achievementShadeOfItAllText": "لقد ربي كل حيوانات الظل للترويض."
|
||||
}
|
||||
|
||||
@@ -409,5 +409,18 @@
|
||||
"backgroundArchaeologicalDigNotes": "Unearth secrets of the ancient past at an Archaeological Dig.",
|
||||
"backgroundScribesWorkshopText": "Scribe's Workshop",
|
||||
"backgroundScribesWorkshopNotes": "Write your next great scroll in a Scribe's Workshop.",
|
||||
"backgrounds022019": "مجموعة 57: تم إصدارها في فبراير 2019"
|
||||
"backgrounds022019": "مجموعة 57: تم إصدارها في فبراير 2019",
|
||||
"backgroundBirthdayPartyText": "حفلة عيد ميلاد",
|
||||
"backgrounds012020": "مجموعة 68: تم طرحه في يناير 2020",
|
||||
"backgroundMedievalKitchenText": "مطبخ القرون الوسطى",
|
||||
"backgroundMedievalKitchenNotes": "اطبخ العاصفة في مطبخ القرون الوسطى.",
|
||||
"backgroundBirthdayPartyNotes": "احتفل بعيد ميلاد ال Habitican المفضل لديك.",
|
||||
"backgroundDuckPondText": "بركة بط",
|
||||
"backgroundOldFashionedBakeryText": "مخبز قديم الطراز",
|
||||
"backgroundValentinesDayFeastingHallText": "قاعة عيد الحب",
|
||||
"backgroundOldFashionedBakeryNotes": "استمتع بالنكهات اللذيذة خارج مخبز قديم الطراز.",
|
||||
"backgroundDuckPondNotes": "أطعم الطيور المائية في بركة البط.",
|
||||
"backgroundValentinesDayFeastingHallNotes": "اشعر بالحب في قاعة احتفالات عيد الحب.",
|
||||
"hideLockedBackgrounds": "إخفاء الخلفيات المقفلة",
|
||||
"backgrounds032019": "SET 58: تم إصداره في مارس 2019"
|
||||
}
|
||||
|
||||
@@ -103,5 +103,6 @@
|
||||
"selectParticipant": "اختر مشارك",
|
||||
"wonChallengeDesc": "<%= إسم التحدي %> إخترتك لتكون الفائز!تم تسجيل فوزك في \"إنجازاتك\".",
|
||||
"yourReward": "مكافئاتك",
|
||||
"filters": "التصفيات"
|
||||
"filters": "التصفيات",
|
||||
"removeTasks": "إزالة المهام"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"languageName": "العربية",
|
||||
"stringNotFound": "سلسلة المحارف '<%= string %>' لم توجد.",
|
||||
"habitica": "Habitica",
|
||||
"onward": "Onward!",
|
||||
"onward": "إلي الأمام!",
|
||||
"done": "Done",
|
||||
"gotIt": "Got it!",
|
||||
"titleTimeTravelers": "مسافرين عبر الزمن",
|
||||
@@ -25,7 +25,7 @@
|
||||
"user": "المستخدم",
|
||||
"market": "المتجر",
|
||||
"newSubscriberItem": "You have new <span class=\"notification-bold-blue\">Mystery Items</span>",
|
||||
"subscriberItemText": "كل شهر يحصل المشتركون على غرض غامض. عادةً يتم إصداره قبل نهاية الشهر بأسبوع. راجع صفحة الويكي \"الغرض الغامض\" للمزيد من المعلومات.",
|
||||
"subscriberItemText": "كل شهر يحصل المشتركون على غرض غامض. عادةً يصبح متاحا في بداية الشهر. راجع صفحة الويكي \"الغرض الغامض\" للمزيد من المعلومات.",
|
||||
"all": "الجميع",
|
||||
"none": "لا شيء",
|
||||
"more": "<%= count %> more",
|
||||
@@ -190,10 +190,28 @@
|
||||
"dismissAll": "Dismiss All",
|
||||
"messages": "Messages",
|
||||
"emptyMessagesLine1": "You don't have any messages",
|
||||
"emptyMessagesLine2": "Send a message to start a conversation!",
|
||||
"emptyMessagesLine2": "يمكنك إرسال رسالة جديدة إلى مستخدم من خلال زيارة ملفه الشخصي والنقر على زر \"رسالة\".",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> sent you a message",
|
||||
"letsgo": "لنذهب!",
|
||||
"selected": "Selected",
|
||||
"howManyToBuy": "How many would you like to buy?",
|
||||
"contactForm": "Contact the Moderation Team"
|
||||
"contactForm": "Contact the Moderation Team",
|
||||
"congratulations": "تهانينا!",
|
||||
"finish": "نهاية",
|
||||
"onboardingAchievs": "إنجازات الإعداد",
|
||||
"reportBugHeaderDescribe": "يُرجى وصف الخطأ الذي تواجهه وسيتواصل معك فريقنا.",
|
||||
"reportEmailText": "سيتم استخدام هذا فقط للاتصال بك بخصوص تقرير الخطأ.",
|
||||
"reportEmailPlaceholder": "عنوان بريدك الإلكتروني",
|
||||
"reportEmailError": "يرجى تقديم عنوان بريد إلكتروني صالح",
|
||||
"reportDescription": "الوصف",
|
||||
"reportDescriptionText": "قم بتضمين لقطات الشاشة أو أخطاء وحدة التحكم بجافا سكريبت إذا كان ذلك مفيدًا.",
|
||||
"reportDescriptionPlaceholder": "صف الخطأ بالتفصيل هنا",
|
||||
"submitBugReport": "إرسال تقرير الخطأ",
|
||||
"reportSent": "تم إرسال تقرير الخطأ!",
|
||||
"askQuestion": "طرح سؤال",
|
||||
"emptyReportBugMessage": "الإبلاغ عن رسالة خطأ مفقودة",
|
||||
"loadEarlierMessages": "تحميل الرسائل السابقة",
|
||||
"demo": "تجريبي",
|
||||
"options": "الإعدادات",
|
||||
"reportSentDescription": "سنعود إليك بمجرد أن تتاح الفرصة لفريقنا للتحقيق في الأمر. شكرا على الإبلاغ عن المشكلة."
|
||||
}
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
"eggsItemType": "بيض",
|
||||
"hatchingPotionsItemType": "جرعات الفقس",
|
||||
"specialItemType": "حاجات خاصة",
|
||||
"lockedItem": "حاجة مقفلة"
|
||||
"lockedItem": "حاجة مقفلة",
|
||||
"petAndMount": "حيوان أليف وحيوان للركوب",
|
||||
"allItems": "كل العناصر"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"needTips": "تحتاج بعض النصائح حول كيفية البدء؟ هنا دليل مباشر!",
|
||||
"step1": "الخطوة ١: أدخل المهام",
|
||||
"webStep1Text": "Habitica لا شيء بدون أهداف حقيقية، لذا أدخل بعض المهام. يمكنك إضافة المزيد في وقت لاحق وأنت تفكر بهم! يمكن إضافة جميع المهام عن طريق النقر على الزر \"إنشاء\" باللون الأخضر.\n* ** إعداد [المهام](http://habitica.wikia.com/wiki/To-Dos): ** أدخل المهام التي تقوم بها مرة واحدة أو نادراً ما في عمود المهام، كل مهمة على حدة. يمكنك أيضاً الضغط على المهام لتحريرها وإضافة قوائم المراجعة وتواريخ الاستحقاق والمزيد!\n* ** إعداد [اليوميات](http://habitica.wikia.com/wiki/Dailies): ** أدخل الأنشطة التي تحتاج فعلها يوميًا أو في يوم معين من الأسبوع أو الشهر أو السنة في عمود اليوميات. انقر على المهمة اليومية لتعديل موعد استحقاقها و/أو تحديد تاريخ البدء. يمكنك أيضًا جعلها مستحقة على أساس متكرر، على سبيل المثال، كل 3 أيام.\n* ** إعداد [العادات](http://habitica.wikia.com/wiki/Habits): ** أدخل العادات التي تريد إقامتها في عمود العادات. يمكنك تحرير العادة لتغييرها إلى عادة جيدة :heavy_plus_sign: أو عادة سيئة :heavy_minus_sign:\n* ** إعداد [المكافآت](http://habitica.wikia.com/wiki/Rewards): ** بالإضافة إلى المكافآت المقدمة في اللعبة، أضف الأنشطة أو الأشياء التي تريد استخدامها كدافع إلى عمود المكافآت. من المهم أن تمنح نفسك فترة راحة أو تسمح ببعض التساهل باعتدال!\n* إذا كنت بحاجة إلى إلهام للمهام التي يمكنك إضافتها، يمكنك الاطلاع على صفحات الويكي عن [نموذج عادات](http://habitica.wikia.com/wiki/Sample_Habits)، و[نموذج يوميات](http://habitica.wikia.com/wiki/Sample_Dailies)، و[نموذج مهام](http://habitica.wikia.com/wiki/Sample_To-Dos)، و[نموذج مكافآت](http://habitica.wikia.com/wiki/Sample_Custom_Rewards).",
|
||||
"webStep1Text": "Habitica لا شيء بدون أهداف حقيقية، لذا أدخل بعض المهام. يمكنك إضافة المزيد في وقت لاحق وأنت تفكر بهم! يمكن إضافة جميع المهام عن طريق النقر على الزر \"إنشاء\" باللون الأخضر.\n* ** إعداد [المهام](https://habitica.wikia.com/wiki/To-Dos): ** أدخل المهام التي تقوم بها مرة واحدة أو نادراً ما في عمود المهام، كل مهمة على حدة. يمكنك أيضاً الضغط على المهام لتحريرها وإضافة قوائم المراجعة وتواريخ الاستحقاق والمزيد!\n* ** إعداد [اليوميات](https://habitica.wikia.com/wiki/Dailies): ** أدخل الأنشطة التي تحتاج فعلها يوميًا أو في يوم معين من الأسبوع أو الشهر أو السنة في عمود اليوميات. انقر على المهمة اليومية لتعديل موعد استحقاقها و/أو تحديد تاريخ البدء. يمكنك أيضًا جعلها مستحقة على أساس متكرر، على سبيل المثال، كل 3 أيام.\n* ** إعداد [العادات](https://habitica.wikia.com/wiki/Habits): ** أدخل العادات التي تريد إقامتها في عمود العادات. يمكنك تحرير العادة لتغييرها إلى عادة جيدة :heavy_plus_sign: أو عادة سيئة :heavy_minus_sign:\n* ** إعداد [المكافآت](https://habitica.wikia.com/wiki/Rewards): ** بالإضافة إلى المكافآت المقدمة في اللعبة، أضف الأنشطة أو الأشياء التي تريد استخدامها كدافع إلى عمود المكافآت. من المهم أن تمنح نفسك فترة راحة أو تسمح ببعض التساهل باعتدال!\n* إذا كنت بحاجة إلى إلهام للمهام التي يمكنك إضافتها، يمكنك الاطلاع على صفحات الويكي عن [نموذج عادات](https://habitica.wikia.com/wiki/Sample_Habits)، و[نموذج يوميات](http://habitica.wikia.com/wiki/Sample_Dailies)، و[نsموذج مهام](https://habitica.wikia.com/wiki/Sample_To-Dos)، و[نموذج مكافآت](https://habitica.wikia.com/wiki/Sample_Custom_Rewards).",
|
||||
"step2": "الخطوة 2: اكسب نقاط عن طريق القيام بأشياء في الحياة الحقيقية",
|
||||
"webStep2Text": "مستوى",
|
||||
"webStep2Text": "الآن ، ابدأ في معالجة أهدافك من القائمة! عندما تكمل المهام وتحقق منها في Habitica ، ستحصل على [الخبرة] (https://habitica.fandom.com/wiki/Experience_Points) ، مما يساعدك على الارتقاء إلى المستوى الأعلى ، و [الذهب] (https: // Habitica. fandom.com/wiki/Gold_Points) ، والذي يسمح لك بشراء مكافأت. إذا وقعت في عادات سيئة أو فاتتك يومياتك ، فستفقد [الصحة] (https://habitica.fandom.com/wiki/Health_Points). بهذه الطريقة ، تعمل أشرطة Habiticaالخبرة والصحة كمؤشر ممتع لتقدمك نحو أهدافك. ستبدأ في رؤية حياتك الحقيقية تتحسن مع تقدم شخصيتك في اللعبة.",
|
||||
"step3": "الخطوة ٣: كيّف واستكشف Habitica",
|
||||
"webStep3Text": "Once you're familiar with the basics, you can get even more out of Habitica with these nifty features:\n * Organize your tasks with [tags](http://habitica.wikia.com/wiki/Tags) (edit a task to add them).\n * Customize your [avatar](http://habitica.wikia.com/wiki/Avatar) by clicking the user icon in the upper-right corner.\n * Buy your [Equipment](http://habitica.wikia.com/wiki/Equipment) under Rewards or from the [Shops](<%= shopUrl %>), and change it under [Inventory > Equipment](<%= equipUrl %>).\n * Connect with other users via the [Tavern](http://habitica.wikia.com/wiki/Tavern).\n * Starting at Level 3, hatch [Pets](http://habitica.wikia.com/wiki/Pets) by collecting [eggs](http://habitica.wikia.com/wiki/Eggs) and [hatching potions](http://habitica.wikia.com/wiki/Hatching_Potions). [Feed](http://habitica.wikia.com/wiki/Food) them to create [Mounts](http://habitica.wikia.com/wiki/Mounts).\n * At level 10: Choose a particular [class](http://habitica.wikia.com/wiki/Class_System) and then use class-specific [skills](http://habitica.wikia.com/wiki/Skills) (levels 11 to 14).\n * Form a party with your friends (by clicking [Party](<%= partyUrl %>) in the navigation bar) to stay accountable and earn a Quest scroll.\n * Defeat monsters and collect objects on [quests](http://habitica.wikia.com/wiki/Quests) (you will be given a quest at level 15).",
|
||||
"webStep3Text": "بمجرد أن تتعرف على الأساسيات ، يمكنك الحصول على المزيد من Habitica بهذه الميزات الرائعة:\n * تنظيم المهام باستخدام [العلامات] (https://habitica.fandom.com/wiki/Tags) (قم بتحرير مهمة لإضافتها).\n * قم بتخصيص [الشخصية] الخاص بك (https://habitica.fandom.com/wiki/Avatar) بالنقر فوق رمز المستخدم في الزاوية العلوية اليمنى.\n * اشتر [المعدات] (https://habitica.fandom.com/wiki/Equipment) بموجب المكافآت أو من [المتاجر] (<٪ = shopUrl٪>) ، وقم بتغييرها ضمن [المخزون> المعدات] (<٪ = equipUrl٪>).\n * تواصل مع مستخدمين آخرين عبر [المطعم] (https://habitica.fandom.com/wiki/Tavern).\n * افقس[الحيوانات الأليفة] (https://habitica.fandom.com/wiki/ Pets) من خلال جمع [البيض] (https://habitica.fandom.com/wiki/Eggs) و [جرعات الفقس] (https: // Habitica.fandom.com/wiki/Hatching_Potions). [موجز] (https://habitica.fandom.com/wiki/Food) لإنشاء [حيوانات للركوب] (https://habitica.fandom.com/wiki/Mounts).\n * في المستوى 10: اختر [فئة] معينة (https://habitica.fandom.com/wiki/Class_System) ثم استخدم [مهارات] خاصة بالفصل (https://habitica.fandom.com/wiki/Skills) (المستويات من 11 إلى 14).\n * كوّن مجموعة مع أصدقائك (بالنقر فوق [حفلة] (<٪ = partyUrl٪>) في شريط التنقل) للبقاء مسؤولاً وكسب تمرير المهام.\n * اهزم الوحوش وجمع الأشياء في [المهام] (https://habitica.fandom.com/wiki/Quests) (ستحصل على مهمة في المستوى 15).",
|
||||
"overviewQuestions": "Have questions? Check out the [FAQ](<%= faqUrl %>)! If your question isn't mentioned there, you can ask for further help in the [Habitica Help guild](<%= helpGuildUrl %>).\n\nGood luck with your tasks!"
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
"beastAchievement": "لقد ربحت \"وحش رئيسي \" إنجاز جمع كل الحيوانات الأليفة!",
|
||||
"beastMasterProgress": "تقدم الوحش الرئيسي",
|
||||
"premiumPotionNoDropExplanation": "لا يمكن استخدام جرعات التفقيس السحرية على البيض المستلم من المهام.الطريقة الوحيدة للحصول على جرعات التفقيس السحرية هي عن طريق شراؤهم بالأسفل.ليس من المقطورات العشوائية.",
|
||||
"dropsExplanationEggs": "أنفق الجواهر لتحصل على المزيد من البيض بسرهة,إذا كنت لاتريد أن تنتظر البيض الأساسي أسقطه, أو كررالتنقيب لإدخارالبيض المنقب <a href=\"http://habitica.fandom.com/wiki/Drops\">Learn more about the drop system.</a>",
|
||||
"dropsExplanation": "احصل على هذه العناصر بشكل أسرع مع الجواهر إذا كنت لا ترغب في انتظار إسقاطها عند إكمال مهمة. <a href=\"http://habitica.fandom.com/wiki/Drops\"> تعرف على المزيد حول نظام الإفلات. </a>",
|
||||
"dropsExplanationEggs": "أنفق الجواهر لتحصل على المزيد من البيض بسرهة,إذا كنت لاتريد أن تنتظر البيض الأساسي أسقطه, أو كررالتنقيب لإدخارالبيض المنقب <a href=\"https://habitica.fandom.com/wiki/Drops\">Learn more about the drop system.</a>",
|
||||
"dropsExplanation": "احصل على هذه العناصر بشكل أسرع مع الجواهر إذا كنت لا ترغب في انتظار إسقاطها عند إكمال مهمة. <a href=\"https://habitica.fandom.com/wiki/Drops\"> تعرف على المزيد حول نظام الإفلات. </a>",
|
||||
"veteranTiger": "النمر المحارب",
|
||||
"veteranWolf": "الذئب المحارب",
|
||||
"etherealLion": "الأسد السماوي",
|
||||
@@ -77,5 +77,26 @@
|
||||
"keyToMountsDesc": "حرر جميع العينات القياسية حتى تتمكن من جمعها مرة أخرى. (لا تتأثر عمليات تثبيت المهام وعمليات التثبيت النادرة.)",
|
||||
"keyToBoth": "مفاتيح رئيسية لبيوت الكلاب",
|
||||
"releasePetsSuccess": "تم إطلاق حيوانك الأليف القياسي!",
|
||||
"mountName": "<%= mount(locale) %> <%= potion(locale) %>"
|
||||
"mountName": "<%= mount(locale) %> <%= potion(locale) %>",
|
||||
"filterByWacky": "أحمق",
|
||||
"sortByColor": "لون",
|
||||
"filterByMagicPotion": "مشروب سحري",
|
||||
"releaseBothSuccess": "لقد تم إطلاق كل حيواناتك الأليفة وحيوانات الركوب القياسية!",
|
||||
"welcomeStable": "مرحبا بكم في الاسطبل!",
|
||||
"mountsReleased": "تم إطلاق حيوانات الركوب القياسية",
|
||||
"hatch": "فقس!",
|
||||
"sortByHatchable": "قابل للفقس",
|
||||
"filterByStandard": "أساسي",
|
||||
"foodTitle": "طعام الحيوانات الاليفة",
|
||||
"welcomeStableText": "مرحبا بكم في الاسطبل! أنا مات ، صاحب الوحش. في كل مرة تكمل فيها مهمة ، سيكون لديك فرصة عشوائية لتلقي بيضة أو جرعة تفقيس لتفقيس الحيوانات الأليفة. عندما تفقس حيوانًا أليفًا ، سيظهر هنا! انقر فوق صورة حيوان أليف لإضافتها إلى صورتك الرمزية. أطعمهم بأطعمة الحيوانات الأليفة التي تجدها وستنمو لتصبح حيوانات ركوب صلبة.",
|
||||
"dragThisFood": "اسحب هذا <%= foodName %> إالي الحيوات وشاهده ينمو!",
|
||||
"filterByQuest": "مغامرة",
|
||||
"standard": "أساسي",
|
||||
"releaseMountsConfirm": "هل أنت متأكد أنك تريد إطلاق كل الحيوانات الأليفة القياسية؟",
|
||||
"releaseMountsSuccess": "لقد تم إطلاق كل حيوانات الركوب القياسية!",
|
||||
"petLikeToEat": "ماذا يحب حيواني الأليف أن يأكل؟",
|
||||
"keyToBothDesc": "حرر جميع الحيوانات الأليفة وحيوانات الركوب القياسية حتى تتمكن من جمعها مرة أخرى. (لا تتأثر Quest Pets / Mounts والحيوانات الأليفة النادرة /حيوانات الركوب.)",
|
||||
"releaseBothConfirm": "هل أنت متأكد من إطلاق حيواناتك الأليفة وحيوانات الركوب القياسية؟",
|
||||
"mountsAndPetsReleased": "الحيوانات الأليفة وحيوانات الركوب القياسية تم إطلاقها",
|
||||
"petLikeToEatText": "ستنمو الحيوانات الأليفة بغض النظر عما تطعمه ، لكنها ستنمو بشكل أسرع إذا أطعمتها طعام الحيوانات الأليفة الذي تفضله أكثر. جرب لمعرفة النمط ، أو شاهد الإجابات هنا: <br/> <a href=\"https://habitica.fandom.com/wiki/Food_Preferences\" target=\"_blank\"> https: //habitica.fandom. com / wiki / Food_Preferences </a>"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level <%= level %>.",
|
||||
"rebirthOrb100": "Used an Orb of Rebirth to start over after attaining Level 100 or higher.",
|
||||
"rebirthOrbNoLevel": "Used an Orb of Rebirth to start over.",
|
||||
"rebirthPop": "Instantly restart your character as a Level 1 Warrior while retaining achievements, collectibles, and equipment. Your tasks and their history will remain but they will be reset to yellow. Your streaks will be removed except from challenge tasks. Your Gold, Experience, Mana, and the effects of all Skills will be removed. All of this will take effect immediately. For more information, see the wiki's <a href='http://habitica.wikia.com/wiki/Orb_of_Rebirth' target='_blank'>Orb of Rebirth</a> page.",
|
||||
"rebirthPop": "أعد شخصيتك على الفور كمحارب من المستوى 1 مع الاحتفاظ بالإنجازات والمقتنيات والمعدات. ستبقى مهامك ومحفوظاتهم ولكن ستتم إعادة تعيينهم إلى اللون الأصفر. ستتم إزالة عدد سلاسلك المستمرة باستثناء المهام التي تنتمي إلى التحديات وخطط المجموعة. ستتم إزالة الذهب ، والخبرة ، ومانا ، وتأثيرات جميع المهارات. كل هذا سيصبح ساري المفعول على الفور. لمزيد من المعلومات ، راجع صفحة ويكي <a href='https://habitica.fandom.com/wiki/Orb_of_Rebirth' target='_blank'> Orb of Rebirth </a>.",
|
||||
"rebirthName": "Orb of Rebirth",
|
||||
"rebirthComplete": "You have been reborn!"
|
||||
"rebirthComplete": "You have been reborn!",
|
||||
"nextFreeRebirth": "<strong><%= الأيام%> days</strong> until <strong>FREE</strong>نجم إعادة الميلاد"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"spellWizardEarthText": "زلزال",
|
||||
"spellWizardEarthNotes": "Your mental power shakes the earth and buffs your Party's Intelligence! (Based on: Unbuffed INT)",
|
||||
"spellWizardFrostText": "صقيع مقشعر",
|
||||
"spellWizardFrostNotes": "With one cast, ice freezes all your streaks so they won't reset to zero tomorrow!",
|
||||
"spellWizardFrostNotes": "عن طريق إطلاق تعويذة واحدة، يقوم الجليد بتجميد كل سلاسل تقدمك حتى لا يتم إعادة تعيينها إلى الصفر غدًا!",
|
||||
"spellWizardFrostAlreadyCast": "You have already cast this today. Your streaks are frozen, and there's no need to cast this again.",
|
||||
"spellWarriorSmashText": "سحقة متوحشة",
|
||||
"spellWarriorSmashNotes": "You make a task more blue/less red and deal extra damage to Bosses! (Based on: STR)",
|
||||
@@ -24,7 +24,7 @@
|
||||
"spellRogueToolsOfTradeNotes": "Your tricky talents buff your whole Party's Perception! (Based on: Unbuffed PER)",
|
||||
"spellRogueStealthText": "تسلل",
|
||||
"spellRogueStealthNotes": "With each cast, a few of your undone Dailies won't cause damage tonight. Their streaks and colors won't change. (Based on: PER)",
|
||||
"spellRogueStealthDaliesAvoided": "<%= originalText %> Number of dailies avoided: <%= number %>.",
|
||||
"spellRogueStealthDaliesAvoided": "<%= originalText %> عدد المهام اليومية التي سيتم تجنبها: <%= number %>.",
|
||||
"spellRogueStealthMaxedOut": "You have already avoided all your dailies; there's no need to cast this again.",
|
||||
"spellHealerHealText": "الضوء المعالج",
|
||||
"spellHealerHealNotes": "Shining light restores your health! (Based on: CON and INT)",
|
||||
@@ -55,5 +55,6 @@
|
||||
"challengeTasksNoCast": "Casting a skill on challenge tasks is not allowed.",
|
||||
"groupTasksNoCast": "Casting a skill on group tasks is not allowed.",
|
||||
"spellNotOwned": "You don't own this skill.",
|
||||
"spellLevelTooHigh": "You must be level <%= level %> to use this skill."
|
||||
}
|
||||
"spellLevelTooHigh": "You must be level <%= level %> to use this skill.",
|
||||
"spellAlreadyCast": "لن يكون لاستخدام هذه المهارة أي تأثير إضافي."
|
||||
}
|
||||
|
||||