Compare commits
222 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 098f53bfa9 | |||
| 4c988691cf | |||
| 1530ab44e9 | |||
| 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 | |||
| 9b9503b141 | |||
| a418752041 | |||
| 7f8e44ff49 | |||
| e0e9381584 | |||
| 18db432f7f | |||
| 30d3892fb4 | |||
| e06a0e5e7f | |||
| 153561dd42 | |||
| 7b067de4b9 | |||
| 9453b1269e | |||
| cf536a82f8 | |||
| 5967e4356c | |||
| 6ebfa976fe | |||
| ddd5f20609 | |||
| a3f61306d3 | |||
| 712b85ce84 | |||
| 9142588ba7 | |||
| b657172a2b | |||
| b790b87ca8 | |||
| 9d3059fc30 | |||
| 647371accc | |||
| 8b084e627e | |||
| 4ac1a3e717 | |||
| a0177fa44d | |||
| 9fec111c4d | |||
| a559c1add8 | |||
| 5868849034 | |||
| c98c7ab26c | |||
| 1ef7924ba5 | |||
| 7651e6a540 | |||
| bb20c44fde | |||
| eb99ca0411 | |||
| 9c24d43a13 | |||
| bf9a7ea7d9 | |||
| ca1dbd2fc4 | |||
| 04107ed6d3 | |||
| 209b7bd1aa | |||
| db354875ee | |||
| 40af14b061 | |||
| 1d048e0c35 | |||
| 6a5f467a35 | |||
| 86b0d6d86c | |||
| 565d33f6a7 | |||
| 588e5dd487 | |||
| d8fbf9420e | |||
| edb606814c | |||
| 32823e3760 | |||
| 48f5ffc997 | |||
| 90375e3bc4 | |||
| c4a92ba384 | |||
| 8f7e5d544e | |||
| 40113b0458 | |||
| 48be0a38bf | |||
| 459b327e2d | |||
| d3fde93762 | |||
| aa81c330af | |||
| 7283d112f4 | |||
| f1b0aa2e7c | |||
| 354f3578a2 | |||
| c5ef803458 | |||
| 40801c0d32 | |||
| e9ca17bbd8 | |||
| 5a638ab4b8 | |||
| 7fa4e6f791 | |||
| 61be42bf05 | |||
| 9d4bf22720 | |||
| 24349bed0a | |||
| 17b93322aa | |||
| ef6d92e7af | |||
| 35ed158dd9 | |||
| 31cbcf53a2 | |||
| 3a2fd28199 | |||
| 1473408752 | |||
| 53babfb9fe | |||
| 64694c3a29 | |||
| 1bd2ec0463 | |||
| 4e9625454c | |||
| a58dd35fbe | |||
| 54088f5374 | |||
| 9eebcf9b16 | |||
| 44722a0d4c | |||
| 7eae0a83f9 | |||
| 294b94206f | |||
| 7e73c336dd | |||
| 82d3545c08 | |||
| e312ea943f | |||
| a4a1595ec7 | |||
| 4b07e3a116 | |||
| 13e645fa4b | |||
| 0d876472a3 | |||
| 9e527f4f35 | |||
| eaa5f821a4 | |||
| a495db8480 | |||
| fa99458ca4 | |||
| 1f81e1971b | |||
| 45fc2b62e3 | |||
| f843564444 | |||
| fb216fba8e | |||
| 603cc93957 | |||
| 63e0875f32 | |||
| 029e41472f | |||
| 1752c08fd9 | |||
| 395d9e7650 | |||
| 61d396204f | |||
| f233c511cc | |||
| 0806391ab8 | |||
| dcaba7f186 | |||
| 59dc97b75f | |||
| 85a9ea726c | |||
| d53813adc7 | |||
| 221dd7a81e | |||
| 99bf6349e1 | |||
| 801b902bb8 | |||
| 072b09e030 | |||
| a5680836bd | |||
| bb9ba61d12 | |||
| a88f97831a | |||
| ae0528e5cd | |||
| 74345adf6b | |||
| 7dbee4caed | |||
| f4feb09fbc | |||
| 6cddb3bf82 | |||
| 248e1c6fe9 | |||
| 6e39c79cff | |||
| 5bf4e18ce8 | |||
| cab4a2a8fa |
@@ -2,6 +2,7 @@
|
||||
"name": "Habitica V3 API Documentation",
|
||||
"title": "Habitica",
|
||||
"url": "https://habitica.com",
|
||||
"version": "3.0.0",
|
||||
"sampleUrl": null,
|
||||
"header": {
|
||||
"title": "Introduction",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import isArray from 'lodash/isArray';
|
||||
import { model as Group } from '../../website/server/models/group';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import * as Tasks from '../../website/server/models/task';
|
||||
|
||||
async function updateTeamTasks (team) {
|
||||
const toSave = [];
|
||||
const teamTasks = await Tasks.Task.find({
|
||||
'group.id': team._id,
|
||||
}).exec();
|
||||
|
||||
const teamBoardTasks = filter(teamTasks, task => !task.userId);
|
||||
const teamUserTasks = filter(teamTasks, task => task.userId);
|
||||
|
||||
for (const boardTask of teamBoardTasks) {
|
||||
if (isArray(boardTask.group.assignedUsers)) {
|
||||
boardTask.group.approval = undefined;
|
||||
boardTask.group.assignedDate = undefined;
|
||||
boardTask.group.assigningUsername = undefined;
|
||||
boardTask.group.sharedCompletion = undefined;
|
||||
|
||||
for (const assignedUserId of boardTask.group.assignedUsers) {
|
||||
const assignedUser = await User.findById(assignedUserId, 'auth'); // eslint-disable-line no-await-in-loop
|
||||
const userTask = find(teamUserTasks, task => task.userId === assignedUserId
|
||||
&& task.group.taskId === boardTask._id);
|
||||
if (!boardTask.group.assignedUsersDetail) boardTask.group.assignedUsersDetail = {};
|
||||
if (userTask && assignedUser) {
|
||||
boardTask.group.assignedUsersDetail[assignedUserId] = {
|
||||
assignedDate: userTask.group.assignedDate,
|
||||
assignedUsername: assignedUser.auth.local.username,
|
||||
assigningUsername: userTask.group.assigningUsername,
|
||||
completed: userTask.completed || false,
|
||||
completedDate: userTask.dateCompleted,
|
||||
};
|
||||
} else if (assignedUser) {
|
||||
boardTask.group.assignedUsersDetail[assignedUserId] = {
|
||||
assignedDate: new Date(),
|
||||
assignedUsername: assignedUser.auth.local.username,
|
||||
assigningUsername: null,
|
||||
completed: false,
|
||||
completedDate: null,
|
||||
};
|
||||
} else {
|
||||
const taskIndex = boardTask.group.assignedUsers.indexOf(assignedUserId);
|
||||
boardTask.group.assignedUsers.splice(taskIndex, 1);
|
||||
}
|
||||
if (userTask) toSave.push(Tasks.Task.findByIdAndDelete(userTask._id));
|
||||
}
|
||||
boardTask.markModified('group');
|
||||
toSave.push(boardTask.save());
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(toSave);
|
||||
}
|
||||
|
||||
export default async function processTeams () {
|
||||
const activeTeams = await Group.find({
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
const taskPromises = activeTeams.map(updateTeamTasks);
|
||||
return Promise.all(taskPromises);
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.238.1",
|
||||
"version": "4.244.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@babel/register": "^7.18.6",
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-env": "^7.19.1",
|
||||
"@babel/register": "^7.18.9",
|
||||
"@google-cloud/trace-agent": "^5.1.6",
|
||||
"@parse/node-apn": "^5.1.3",
|
||||
"@slack/webhook": "^6.1.0",
|
||||
"accepts": "^1.3.8",
|
||||
"amazon-payments": "^0.2.9",
|
||||
"amplitude": "^6.0.0",
|
||||
"apidoc": "^0.52.0",
|
||||
"apidoc": "^0.53.0",
|
||||
"apple-auth": "^1.0.7",
|
||||
"bcrypt": "^5.0.1",
|
||||
"body-parser": "^1.20.0",
|
||||
@@ -61,7 +61,7 @@
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.3.7",
|
||||
"rate-limiter-flexible": "^2.3.10",
|
||||
"redis": "^3.1.2",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"remove-markdown": "^0.5.0",
|
||||
@@ -121,7 +121,7 @@
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.3.4",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.6",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon": "^13.0.2",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import forEach from 'lodash/forEach';
|
||||
import { model as Group } from '../website/server/models/group';
|
||||
import { model as User } from '../website/server/models/user';
|
||||
import * as Tasks from '../website/server/models/task';
|
||||
import { daysSince, shouldDo } from '../website/common/script/cron';
|
||||
|
||||
const TASK_VALUE_CHANGE_FACTOR = 0.9747;
|
||||
const MIN_TASK_VALUE = -47.27;
|
||||
|
||||
async function updateTeamTasks (team) {
|
||||
const toSave = [];
|
||||
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
||||
|
||||
if (!teamLeader) { // why would this happen?
|
||||
teamLeader = {
|
||||
preferences: { }, // when options are sanitized this becomes CDS 0 at UTC
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!team.cron || !team.cron.lastProcessed
|
||||
|| daysSince(team.cron.lastProcessed, teamLeader.preferences) > 0
|
||||
) {
|
||||
const tasks = await Tasks.Task.find({
|
||||
'group.id': team._id,
|
||||
userId: { $exists: false },
|
||||
$or: [
|
||||
{ type: 'todo', completed: false },
|
||||
{ type: { $in: ['habit', 'daily'] } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
const tasksByType = {
|
||||
habits: [], dailys: [], todos: [], rewards: [],
|
||||
};
|
||||
forEach(tasks, task => tasksByType[`${task.type}s`].push(task));
|
||||
|
||||
forEach(tasksByType.habits, habit => {
|
||||
if (!(habit.up && habit.down) && habit.value !== 0) {
|
||||
habit.value *= 0.5;
|
||||
if (Math.abs(habit.value) < 0.1) habit.value = 0;
|
||||
toSave.push(habit.save());
|
||||
}
|
||||
});
|
||||
forEach(tasksByType.todos, todo => {
|
||||
if (!todo.completed) {
|
||||
const delta = TASK_VALUE_CHANGE_FACTOR ** todo.value;
|
||||
todo.value -= delta;
|
||||
if (todo.value < MIN_TASK_VALUE) todo.value = MIN_TASK_VALUE;
|
||||
toSave.push(todo.save());
|
||||
}
|
||||
});
|
||||
forEach(tasksByType.dailys, daily => {
|
||||
let processChecklist = false;
|
||||
let assignments = 0;
|
||||
let completions = 0;
|
||||
for (const assignedUser in daily.group.assignedUsersDetail) {
|
||||
if (Object.prototype.hasOwnProperty.call(daily.group.assignedUsersDetail, assignedUser)) {
|
||||
assignments += 1;
|
||||
if (daily.group.assignedUsersDetail[assignedUser].completed) {
|
||||
completions += 1;
|
||||
daily.group.assignedUsersDetail[assignedUser].completed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (completions > 0) daily.markModified('group.assignedUsersDetail');
|
||||
if (daily.completed) {
|
||||
processChecklist = true;
|
||||
daily.completed = false;
|
||||
} else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) {
|
||||
processChecklist = true;
|
||||
const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value;
|
||||
if (assignments > 0) {
|
||||
daily.value -= ((completions / assignments) * delta);
|
||||
}
|
||||
if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE;
|
||||
}
|
||||
daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences);
|
||||
if (processChecklist && daily.checklist.length > 0) {
|
||||
daily.checklist.forEach(i => { i.completed = false; });
|
||||
}
|
||||
toSave.push(daily.save());
|
||||
});
|
||||
|
||||
if (!team.cron) team.cron = {};
|
||||
team.cron.lastProcessed = new Date();
|
||||
toSave.push(team.save());
|
||||
}
|
||||
|
||||
return Promise.all(toSave);
|
||||
}
|
||||
|
||||
export default async function processTeamsCron () {
|
||||
const activeTeams = await Group.find({
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
const cronPromises = activeTeams.map(updateTeamTasks);
|
||||
return Promise.all(cronPromises);
|
||||
}
|
||||
@@ -13,11 +13,6 @@ function getUser () {
|
||||
username: 'username',
|
||||
email: 'email@email',
|
||||
},
|
||||
facebook: {
|
||||
emails: [{
|
||||
value: 'email@facebook',
|
||||
}],
|
||||
},
|
||||
google: {
|
||||
emails: [{
|
||||
value: 'email@google',
|
||||
@@ -62,30 +57,12 @@ describe('emails', () => {
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [facebook users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.auth.local.username);
|
||||
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [google users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
@@ -103,7 +80,6 @@ describe('emails', () => {
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.facebook.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
@@ -118,7 +94,6 @@ describe('emails', () => {
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook;
|
||||
delete user.auth.google;
|
||||
delete user.auth.apple;
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ describe('Password Utilities', () => {
|
||||
it('returns false if the user has no local auth', async () => {
|
||||
const user = await generateUser({
|
||||
auth: {
|
||||
facebook: {},
|
||||
google: {},
|
||||
},
|
||||
});
|
||||
const res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}); */
|
||||
}
|
||||
|
||||
@@ -570,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;
|
||||
@@ -1439,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;
|
||||
@@ -1839,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;
|
||||
@@ -2126,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;
|
||||
@@ -3000,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;
|
||||
@@ -3405,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;
|
||||
@@ -18510,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;
|
||||
@@ -18520,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;
|
||||
@@ -19725,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;
|
||||
@@ -19735,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;
|
||||
@@ -22825,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;
|
||||
@@ -22995,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;
|
||||
@@ -23120,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;
|
||||
@@ -23275,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;
|
||||
@@ -23445,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;
|
||||
@@ -23570,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;
|
||||
@@ -23725,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;
|
||||
@@ -23885,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;
|
||||
@@ -24045,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;
|
||||
@@ -27015,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;
|
||||
@@ -36693,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;
|
||||
@@ -37108,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;
|
||||
@@ -37623,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;
|
||||
@@ -38038,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;
|
||||
@@ -38303,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;
|
||||
@@ -38958,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;
|
||||
@@ -39443,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;
|
||||
@@ -40663,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;
|
||||
@@ -41238,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;
|
||||
@@ -41758,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;
|
||||
@@ -42173,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;
|
||||
@@ -42688,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;
|
||||
@@ -43103,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;
|
||||
@@ -43368,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;
|
||||
@@ -44023,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;
|
||||
@@ -44508,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;
|
||||
@@ -45728,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;
|
||||
@@ -46303,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;
|
||||
@@ -46828,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;
|
||||
@@ -47243,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;
|
||||
@@ -47758,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;
|
||||
@@ -48173,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;
|
||||
@@ -48438,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;
|
||||
@@ -49098,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;
|
||||
@@ -49583,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;
|
||||
@@ -50803,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;
|
||||
@@ -51378,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;
|
||||
@@ -51908,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;
|
||||
@@ -52338,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;
|
||||
@@ -52873,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;
|
||||
@@ -53303,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;
|
||||
@@ -53583,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;
|
||||
@@ -54258,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;
|
||||
@@ -54758,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;
|
||||
@@ -56003,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;
|
||||
@@ -56598,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;
|
||||
@@ -56928,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;
|
||||
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 446 B |
|
After Width: | Height: | Size: 741 B |
|
After Width: | Height: | Size: 979 B |
@@ -1,9 +1,56 @@
|
||||
.create-task-area {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
right: 12px;
|
||||
top: -24px;
|
||||
z-index: 998;
|
||||
align-items: center;
|
||||
|
||||
.dropdown {
|
||||
width: 140px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
|
||||
background-color: $white;
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
|
||||
.task-icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.task-label {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
.svg-icon {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
.svg-icon {
|
||||
color: $gray-200;
|
||||
|
||||
&.icon-habit {
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.icon-daily {
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.icon-todo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.icon-reward {
|
||||
width: 26px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide-tasks-btns-leave-active, .slide-tasks-btns-enter-active {
|
||||
@@ -17,70 +64,14 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.diamond-btn {
|
||||
margin-left: 24px;
|
||||
background: $white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
cursor: pointer;
|
||||
color: $gray-200;
|
||||
transform: rotate(45deg);
|
||||
|
||||
&:hover:not(.create-btn) {
|
||||
color: $purple-400;
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
&.icon-habit {
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.icon-daily {
|
||||
width: 21.6px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&.icon-todo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&.icon-reward {
|
||||
width: 23.4px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
color: $white;
|
||||
background-color: $green-100;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: $purple-200;
|
||||
height: 32px;
|
||||
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform: rotate(-45deg);
|
||||
transition: transform 0.3s cubic-bezier(0, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
&.open {
|
||||
background: $gray-200 !important;
|
||||
|
||||
.svg-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
color: $purple-500;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
&-bg {
|
||||
background: $maroon-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $maroon-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $maroon-100 !important; }
|
||||
&-headings { color: $white; }
|
||||
&-headings { color: $white !important; }
|
||||
&-icon { color: $maroon-100 !important; }
|
||||
&-text { color: $red-1 !important; }
|
||||
&-content {
|
||||
@@ -61,7 +61,7 @@
|
||||
&-bg {
|
||||
background: $red-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $red-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -92,7 +92,7 @@
|
||||
&-bg {
|
||||
background: $orange-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $orange-100 !important; }
|
||||
&-inner-habit { background: rgba($orange-1, 0.25) !important; }
|
||||
@@ -123,7 +123,7 @@
|
||||
&-bg {
|
||||
background: $yellow-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $yellow-100 !important; }
|
||||
&-inner-habit { background: rgba($yellow-1, 0.25) !important; }
|
||||
@@ -154,7 +154,7 @@
|
||||
&-bg {
|
||||
background: $green-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $green-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -186,7 +186,7 @@
|
||||
&-bg {
|
||||
background: $teal-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $teal-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -217,7 +217,7 @@
|
||||
&-bg {
|
||||
background: $blue-100 !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $blue-100 !important; }
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -249,7 +249,7 @@
|
||||
&-bg {
|
||||
background: $purple-task !important;
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
&-inner-daily-todo { background: rgba($white, 0.5) !important; }
|
||||
@@ -258,7 +258,7 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $purple-300 !important; }
|
||||
&-headings { color: $white; }
|
||||
&-headings { color: $white !important; }
|
||||
&-icon { color: $purple-300 !important; }
|
||||
&-text { color: $black !important; }
|
||||
&-content {
|
||||
@@ -281,11 +281,11 @@
|
||||
background: rgba($yellow-100, 0.15) !important;
|
||||
.small-text { color: $yellow-1 !important; }
|
||||
|
||||
&:hover { background: rgba($yellow-100, 0.25) !important; }
|
||||
&:hover:not(.task-not-scoreable) { background: rgba($yellow-100, 0.25) !important; }
|
||||
}
|
||||
&-bg-noninteractive {
|
||||
background: rgba($yellow-100, 0.15) !important;
|
||||
.small-text { color: $yellow-1 !important; }
|
||||
background-color: $gray-500 !important;
|
||||
.small-text { color: $gray-100 !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,7 +312,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $gray-200 !important;
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive {
|
||||
background: $gray-200 !important;
|
||||
@@ -323,7 +323,7 @@
|
||||
background: $gray-600;
|
||||
|
||||
.task-title, .task-notes {
|
||||
color: $gray-300 !important;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="16" viewBox="0 0 8 16">
|
||||
<path fill-rule="evenodd" d="M7.145 8.006H4.903V16H1.58V8.006H0V5.182h1.58V3.354C1.58 2.045 2.202 0 4.933 0l2.461.01v2.742H5.608c-.291 0-.705.145-.705.77v1.66h2.533l-.291 2.824z"/>
|
||||
<svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4C1.79,0,0,1.79,0,4V20c0,2.21,1.79,4,4,4H20c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4Zm-3.72,6.66h-1.26c-1.24,0-1.63,.77-1.63,1.56v1.88h2.78l-.44,2.9h-2.33v7h-3.13v-7h-2.54v-2.9h2.54v-2.21c0-2.51,1.5-3.9,3.78-3.9,1.1,0,2.24,.2,2.24,.2v2.47Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 354 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 0c2.173 0 2.445.01 3.298.048.852.04 1.433.174 1.942.372.526.205.973.478 1.418.922.444.445.717.892.922 1.418.198.509.333 1.09.372 1.942C15.99 5.555 16 5.827 16 8s-.01 2.445-.048 3.298c-.04.852-.174 1.433-.372 1.942a3.924 3.924 0 0 1-.922 1.418 3.924 3.924 0 0 1-1.418.922c-.509.198-1.09.333-1.942.372-.853.04-1.125.048-3.298.048s-2.445-.009-3.298-.048c-.852-.04-1.433-.174-1.942-.372a3.924 3.924 0 0 1-1.418-.922A3.924 3.924 0 0 1 .42 13.24c-.198-.509-.333-1.09-.372-1.942C.01 10.445 0 10.173 0 8s.01-2.445.048-3.298C.088 3.85.222 3.269.42 2.76c.205-.526.478-.973.922-1.418A3.924 3.924 0 0 1 2.76.42C3.269.222 3.85.087 4.702.048 5.555.01 5.827 0 8 0zm0 3.892a4.108 4.108 0 1 0 0 8.216 4.108 4.108 0 0 0 0-8.216zm5.23-.162a.96.96 0 1 0-1.92 0 .96.96 0 0 0 1.92 0zM8 10.667a2.666 2.666 0 1 1 0-5.333 2.666 2.666 0 0 1 0 5.333z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M14.67,12A2.67,2.67,0,1,1,12,9.33,2.67,2.67,0,0,1,14.67,12Zm3.89,0c0,1.16.1,3.66-.32,4.71a2.7,2.7,0,0,1-1.52,1.52c-1.06.42-3.55.33-4.72.33s-3.66.09-4.71-.33a2.65,2.65,0,0,1-1.52-1.52c-.42-1.05-.33-3.55-.33-4.71s-.09-3.67.33-4.72A2.65,2.65,0,0,1,7.29,5.76c1-.42,3.55-.32,4.71-.32a18,18,0,0,1,4.72.32,2.7,2.7,0,0,1,1.52,1.52C18.66,8.34,18.56,10.83,18.56,12ZM16.1,12A4.1,4.1,0,1,0,12,16.1,4.09,4.09,0,0,0,16.1,12Zm1.13-4.27a1,1,0,1,0-1,1A1,1,0,0,0,17.23,7.73ZM24,4V20a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V4A4,4,0,0,1,4,0H20A4,4,0,0,1,24,4ZM19.94,8.7a4.71,4.71,0,0,0-1.29-3.35A4.71,4.71,0,0,0,15.3,4.06C14,4,10,4,8.7,4.06A4.76,4.76,0,0,0,5.35,5.34,4.75,4.75,0,0,0,4.06,8.7C4,10,4,14,4.06,15.3a4.73,4.73,0,0,0,1.29,3.35A4.77,4.77,0,0,0,8.7,19.94c1.32.08,5.28.08,6.6,0a4.71,4.71,0,0,0,3.35-1.29,4.73,4.73,0,0,0,1.29-3.35C20,14,20,10,19.94,8.7Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 914 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m12.707 5.707-1.414-1.414L8 7.586 6.707 6.293 5.293 7.707 8 10.414l4.707-4.707zM16 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2h2v2h10V2H4v2.5h2l-3 3-3-3h2V2a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" fill="#878190" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM16.68,19a4.47,4.47,0,0,1-3,1C9.86,20,9,17.22,9,15.61v-4.5H7.56a.31.31,0,0,1-.31-.32V8.67a.52.52,0,0,1,.35-.5,4,4,0,0,0,2.63-3.66c0-.34.21-.51.51-.51H13a.31.31,0,0,1,.32.31v3.6h2.59a.31.31,0,0,1,.31.31v2.55a.31.31,0,0,1-.31.32H13.25v4.16c0,1.07.74,1.67,2.13,1.12a.67.67,0,0,1,.4-.07.36.36,0,0,1,.23.25l.68,2C16.75,18.71,16.8,18.89,16.68,19Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 516 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="13" viewBox="0 0 16 13">
|
||||
<path fill-rule="evenodd" d="M14.362 3.238c.007.141.01.281.01.424 0 4.338-3.302 9.34-9.34 9.34A9.284 9.284 0 0 1 0 11.527c.257.029.518.045.783.045a6.576 6.576 0 0 0 4.076-1.404 3.288 3.288 0 0 1-3.065-2.28 3.312 3.312 0 0 0 1.481-.056A3.288 3.288 0 0 1 .642 4.613v-.041c.444.246.949.393 1.488.41A3.28 3.28 0 0 1 .67 2.25c0-.602.162-1.166.444-1.651a9.315 9.315 0 0 0 6.766 3.43A3.28 3.28 0 0 1 11.078 0c.943 0 1.797.398 2.395 1.035a6.565 6.565 0 0 0 2.085-.797 3.289 3.289 0 0 1-1.443 1.816A6.543 6.543 0 0 0 16 1.539a6.665 6.665 0 0 1-1.638 1.699"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM18.36,8.74c0,.14,0,.29,0,.43A9.34,9.34,0,0,1,4,17a6.85,6.85,0,0,0,.79,0,6.57,6.57,0,0,0,4.07-1.4A3.29,3.29,0,0,1,5.8,13.39a4.1,4.1,0,0,0,.62,0,3.49,3.49,0,0,0,.86-.11,3.28,3.28,0,0,1-2.63-3.22v0a3.35,3.35,0,0,0,1.48.42A3.29,3.29,0,0,1,4.67,7.76,3.22,3.22,0,0,1,5.12,6.1a9.3,9.3,0,0,0,6.76,3.43,3.67,3.67,0,0,1-.08-.75,3.28,3.28,0,0,1,5.67-2.24,6.54,6.54,0,0,0,2.08-.79,3.22,3.22,0,0,1-1.44,1.8A6.67,6.67,0,0,0,20,7.05,7.31,7.31,0,0,1,18.36,8.74Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 622 B |
@@ -1,20 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<defs>
|
||||
<path id="rt3mruthma" d="M11.2 13.2c0-1.231-.467-2.35-1.23-3.2h1.23c1.765 0 3.2 1.435 3.2 3.2h-3.2zm-9.6 0c0-1.765 1.435-3.2 3.2-3.2h1.6c1.765 0 3.2 1.435 3.2 3.2h-8zm4-9.6C6.926 3.6 8 4.674 8 6c0 1.326-1.074 2.4-2.4 2.4-1.326 0-2.4-1.074-2.4-2.4 0-1.326 1.074-2.4 2.4-2.4zm3.452.415C9.436 3.754 9.9 3.6 10.4 3.6c1.326 0 2.4 1.074 2.4 2.4 0 1.326-1.074 2.4-2.4 2.4-.5 0-.964-.154-1.348-.415.34-.587.548-1.26.548-1.985 0-.726-.209-1.398-.548-1.985zm4.154 4.829C13.942 8.118 14.4 7.112 14.4 6c0-2.206-1.794-4-4-4-.904 0-1.73.313-2.4.82C7.33 2.314 6.504 2 5.6 2c-2.206 0-4 1.794-4 4 0 1.112.458 2.118 1.194 2.844C1.146 9.604 0 11.266 0 13.2c0 .884.716 1.6 1.6 1.6h12.8c.884 0 1.6-.716 1.6-1.6 0-1.934-1.146-3.596-2.794-4.356z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g>
|
||||
<g transform="translate(-1228 -1456) translate(1216 1416) translate(12 40)">
|
||||
<mask id="6szqyv7o1b" fill="#fff">
|
||||
<use xlink:href="#rt3mruthma"/>
|
||||
</mask>
|
||||
<use fill="#878190" xlink:href="#rt3mruthma"/>
|
||||
<g fill="#878190" mask="url(#6szqyv7o1b)">
|
||||
<path d="M0 0H16V16H0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12.8"><path d="M11.2,12.8A4.79,4.79,0,0,0,10,9.6H11.2a3.21,3.21,0,0,1,3.2,3.2Zm-9.6,0A3.21,3.21,0,0,1,4.8,9.6H6.4a3.21,3.21,0,0,1,3.2,3.2Zm4-9.6A2.4,2.4,0,1,1,3.2,5.6,2.39,2.39,0,0,1,5.6,3.2Zm3.45.42A2.35,2.35,0,0,1,10.4,3.2a2.4,2.4,0,1,1,0,4.8,2.35,2.35,0,0,1-1.35-.42,3.84,3.84,0,0,0,0-4Zm4.16,4.82A4,4,0,0,0,8,2.42,4,4,0,0,0,5.6,1.6,4,4,0,0,0,2.79,8.44,4.82,4.82,0,0,0,0,12.8a1.6,1.6,0,0,0,1.6,1.6H14.4A1.6,1.6,0,0,0,16,12.8a4.82,4.82,0,0,0-2.79-4.36Z" transform="translate(0 -1.6)" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 569 B |
@@ -1,21 +1,5 @@
|
||||
<template>
|
||||
<div class="form">
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('facebook')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.facebookIcon"
|
||||
></div>
|
||||
<span>{{ registering
|
||||
? $t('signUpWithSocial', {social: 'Facebook'})
|
||||
: $t('loginWithSocial', {social: 'Facebook'}) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12">
|
||||
<div
|
||||
@@ -243,7 +227,6 @@ import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
|
||||
@@ -260,7 +243,6 @@ export default {
|
||||
};
|
||||
|
||||
data.icons = Object.freeze({
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
});
|
||||
@@ -308,8 +290,6 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
|
||||
@@ -621,7 +621,6 @@ import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import exclamation from '@/assets/svg/exclamation.svg';
|
||||
import gryphon from '@/assets/svg/gryphon.svg';
|
||||
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
|
||||
@@ -644,7 +643,6 @@ export default {
|
||||
exclamation,
|
||||
gryphon,
|
||||
habiticaIcon,
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
});
|
||||
@@ -734,8 +732,6 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
@@ -757,8 +753,7 @@ export default {
|
||||
}, 500),
|
||||
sanitizeRedirect (redirect) {
|
||||
if (!redirect) return '/';
|
||||
let sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
sanitizedString = `/${sanitizedString}`;
|
||||
const sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
return sanitizedString;
|
||||
},
|
||||
async register () {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<customize-options
|
||||
:items="set.options"
|
||||
:current-value="user.preferences.hair.color"
|
||||
:full-set="!hideSet(set) && !userOwnsSet('hair', set.keys, 'color')"
|
||||
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
|
||||
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<customize-options
|
||||
:items="set.options"
|
||||
:current-value="user.preferences.skin"
|
||||
:full-set="!hideSet(set) && !userOwnsSet('skin', set.keys)"
|
||||
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
|
||||
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -112,10 +112,8 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'group._id': {
|
||||
async groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
'group._id': function groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -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>
|
||||
@@ -68,7 +68,8 @@
|
||||
</b-popover>
|
||||
<div
|
||||
class="spell-border"
|
||||
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl }"
|
||||
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl,
|
||||
'insufficient-mana': user.stats.mp < skill.mana }"
|
||||
>
|
||||
<div
|
||||
class="spell"
|
||||
@@ -87,19 +88,6 @@
|
||||
<div>Level {{ skill.lvl }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="spellDisabled(key) === true"
|
||||
class="mana"
|
||||
>
|
||||
<div class="mana-text">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.mana"
|
||||
></div>
|
||||
<div>{{ skill.mana }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="mana"
|
||||
@@ -200,7 +188,7 @@
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
&:hover:not(.disabled):not(.insufficient-mana) {
|
||||
background-color: $purple-400;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16),
|
||||
@@ -216,6 +204,10 @@
|
||||
background-color: rgba(26, 24, 29, 0.5);
|
||||
}
|
||||
|
||||
.mana-text {
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
.level {
|
||||
color: $white;
|
||||
font-weight: normal;
|
||||
@@ -223,6 +215,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.insufficient-mana:not(.disabled) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.spell {
|
||||
background: $white;
|
||||
border-radius: 4px;
|
||||
@@ -465,7 +461,7 @@ export default {
|
||||
spellDisabled (skill) {
|
||||
const incompleteDailiesDue = this
|
||||
.getUnfilteredTaskList('daily')
|
||||
.filter(daily => !daily.completed && daily.isDue)
|
||||
.filter(daily => !daily.completed && !daily.group.id && daily.isDue)
|
||||
.length;
|
||||
|
||||
if (skill === 'frost' && this.user.stats.buffs.streaks) {
|
||||
@@ -481,7 +477,9 @@ export default {
|
||||
skillNotes (skill) {
|
||||
let notes = skill.notes();
|
||||
|
||||
if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
|
||||
if (this.user.stats.lvl < skill.lvl) {
|
||||
notes = this.$t('spellLevelTooHigh', { level: skill.lvl });
|
||||
} else if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
|
||||
notes = this.$t('spellAlreadyCast');
|
||||
} else if (skill.key === 'stealth' && this.spellDisabled(skill.key)) {
|
||||
notes = this.$t('spellAlreadyCast');
|
||||
|
||||
@@ -3,47 +3,39 @@
|
||||
<div
|
||||
class="task transition"
|
||||
:class="[{
|
||||
'groupTask': task.group.id,
|
||||
'task-not-editable': !teamManagerAccess},
|
||||
`type_${task.type}`
|
||||
'groupTask': task.group.id,
|
||||
'task-not-editable': !teamManagerAccess,
|
||||
'task-not-scoreable': showTaskLockIcon,
|
||||
}, `type_${task.type}`
|
||||
]"
|
||||
@click="castEnd($event, task)"
|
||||
>
|
||||
<approval-header
|
||||
v-if="task.group.id"
|
||||
:task="task"
|
||||
:group="group"
|
||||
/>
|
||||
<div
|
||||
class="d-flex"
|
||||
:class="{'task-not-scoreable': isUser !== true || task.group.approval.requested
|
||||
&& !(task.group.approval.approved && task.type === 'habit')}"
|
||||
:class="{'task-not-scoreable': showTaskLockIcon }"
|
||||
>
|
||||
<!-- Habits left side control-->
|
||||
<div
|
||||
v-if="task.type === 'habit'"
|
||||
class="left-control d-flex justify-content-center pt-3"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-bottom-box': task.group.id && !isOpenTask,
|
||||
'control-top-box': approvalsClass
|
||||
}, controlClass.up.bg]"
|
||||
>
|
||||
<div
|
||||
class="task-control habit-control"
|
||||
:class="[{
|
||||
'habit-control-positive-enabled': task.up && isUser,
|
||||
'habit-control-positive-disabled': !task.up && isUser,
|
||||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
'habit-control-positive-enabled': task.up && !showTaskLockIcon,
|
||||
'habit-control-positive-disabled': !task.up && !showTaskLockIcon,
|
||||
'task-not-scoreable': showTaskLockIcon,
|
||||
}, controlClass.up.inner]"
|
||||
tabindex="0"
|
||||
@click="(isUser && task.up && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('up') : null"
|
||||
@keypress.enter="(isUser && task.up && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('up') : null"
|
||||
@click="score('up')"
|
||||
@keypress.enter="score('up')"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon lock"
|
||||
:class="task.up ? controlClass.up.icon : 'positive'"
|
||||
v-html="icons.lock"
|
||||
@@ -60,20 +52,21 @@
|
||||
v-if="task.type === 'daily' || task.type === 'todo'"
|
||||
class="left-control d-flex justify-content-center"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-bottom-box': task.group.id && !isOpenTask,
|
||||
'control-top-box': approvalsClass}, controlClass.bg]"
|
||||
>
|
||||
<div
|
||||
class="task-control daily-todo-control"
|
||||
:class="controlClass.inner"
|
||||
:class="[
|
||||
{ 'task-not-scoreable': showTaskLockIcon },
|
||||
controlClass.inner,
|
||||
]"
|
||||
tabindex="0"
|
||||
@click="isUser && !task.group.approval.requested
|
||||
? score(task.completed ? 'down' : 'up' ) : null"
|
||||
@keypress.enter="isUser && !task.group.approval.requested
|
||||
? score(task.completed ? 'down' : 'up' ) : null"
|
||||
@click="score(showCheckIcon ? 'down' : 'up' )"
|
||||
@keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon lock"
|
||||
:class="controlClass.icon"
|
||||
v-html="icons.lock"
|
||||
@@ -82,7 +75,7 @@
|
||||
v-else
|
||||
class="svg-icon check"
|
||||
:class="{
|
||||
'display-check-icon': task.completed || task.group.approval.requested,
|
||||
'display-check-icon': showCheckIcon,
|
||||
[controlClass.checkbox]: true,
|
||||
}"
|
||||
v-html="icons.check"
|
||||
@@ -95,8 +88,8 @@
|
||||
:class="contentClass"
|
||||
>
|
||||
<div
|
||||
class="task-clickable-area"
|
||||
:class="{ 'cursor-auto': !isUser && !teamManagerAccess }"
|
||||
class="task-clickable-area pt-1 pl-75 pb-0"
|
||||
:class="{ 'cursor-auto': !teamManagerAccess }"
|
||||
tabindex="0"
|
||||
@click="edit($event, task)"
|
||||
@keypress.enter="edit($event, task)"
|
||||
@@ -105,14 +98,14 @@
|
||||
<h3
|
||||
v-markdown="task.text"
|
||||
class="task-title markdown"
|
||||
:class="{ 'has-notes': task.notes || (!isUser && task.group.managerNotes)}"
|
||||
:class="{ 'has-notes': task.notes }"
|
||||
></h3>
|
||||
<menu-dropdown
|
||||
v-if="!isRunningYesterdailies && showOptions"
|
||||
ref="taskDropdown"
|
||||
v-b-tooltip.hover.top="$t('options')"
|
||||
tabindex="0"
|
||||
class="task-dropdown"
|
||||
class="task-dropdown mr-1"
|
||||
:right="task.type === 'reward'"
|
||||
>
|
||||
<div slot="dropdown-toggle">
|
||||
@@ -138,7 +131,6 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUser"
|
||||
class="dropdown-item"
|
||||
tabindex="0"
|
||||
@click="moveToTop"
|
||||
@@ -153,7 +145,6 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUser"
|
||||
class="dropdown-item"
|
||||
tabindex="0"
|
||||
@click="moveToBottom"
|
||||
@@ -186,7 +177,7 @@
|
||||
</menu-dropdown>
|
||||
</div>
|
||||
<div
|
||||
v-markdown="displayNotes"
|
||||
v-markdown="task.notes"
|
||||
class="task-notes small-text"
|
||||
:class="{'has-checklist': task.notes && hasChecklist}"
|
||||
></div>
|
||||
@@ -220,7 +211,7 @@
|
||||
v-if="!task.collapseChecklist"
|
||||
:key="item.id"
|
||||
class="custom-control custom-checkbox checklist-item"
|
||||
:class="{'checklist-item-done': item.completed, 'cursor-auto': !isUser}"
|
||||
:class="{'checklist-item-done': item.completed, 'cursor-auto': showTaskLockIcon}"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<input
|
||||
@@ -229,14 +220,14 @@
|
||||
tabindex="0"
|
||||
type="checkbox"
|
||||
:checked="item.completed"
|
||||
:disabled="castingSpell || !isUser"
|
||||
:disabled="castingSpell || showTaskLockIcon"
|
||||
@change="toggleChecklistItem(item)"
|
||||
@keypress.enter="toggleChecklistItem(item)"
|
||||
>
|
||||
<label
|
||||
v-markdown="item.text"
|
||||
class="custom-control-label"
|
||||
:class="{ 'cursor-auto': !isUser }"
|
||||
:class="{ 'cursor-auto': showTaskLockIcon }"
|
||||
:for="`checklist-${item.id}-${random}`"
|
||||
></label>
|
||||
</div>
|
||||
@@ -316,7 +307,7 @@
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasTags"
|
||||
v-if="hasTags && !task.group.id"
|
||||
:id="`tags-icon-${task._id}`"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
@@ -326,7 +317,7 @@
|
||||
></div>
|
||||
</div>
|
||||
<b-popover
|
||||
v-if="hasTags"
|
||||
v-if="hasTags && !task.group.id"
|
||||
:target="`tags-icon-${task._id}`"
|
||||
triggers="hover"
|
||||
placement="bottom"
|
||||
@@ -356,25 +347,22 @@
|
||||
v-if="task.type === 'habit'"
|
||||
class="right-control d-flex justify-content-center pt-3"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-bottom-box': task.group.id && !isOpenTask,
|
||||
'control-top-box': approvalsClass}, controlClass.down.bg]"
|
||||
>
|
||||
<div
|
||||
class="task-control habit-control"
|
||||
:class="[{
|
||||
'habit-control-negative-enabled': task.down && isUser,
|
||||
'habit-control-negative-disabled': !task.down && isUser,
|
||||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
'habit-control-negative-enabled': task.down && !showTaskLockIcon,
|
||||
'habit-control-negative-disabled': !task.down && !showTaskLockIcon,
|
||||
'task-not-scoreable': showTaskLockIcon,
|
||||
}, controlClass.down.inner]"
|
||||
tabindex="0"
|
||||
@click="(isUser && task.down && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('down') : null"
|
||||
@keypress.enter="(isUser && task.down && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('down') : null"
|
||||
@click="score('down')"
|
||||
@keypress.enter="score('down')"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon lock"
|
||||
:class="task.down ? controlClass.down.icon : 'negative'"
|
||||
v-html="icons.lock"
|
||||
@@ -390,13 +378,22 @@
|
||||
<div
|
||||
v-if="task.type === 'reward'"
|
||||
class="right-control d-flex align-items-center justify-content-center reward-control"
|
||||
:class="controlClass.bg"
|
||||
:class="[
|
||||
{ 'task-not-scoreable': showTaskLockIcon },
|
||||
controlClass.bg,
|
||||
]"
|
||||
tabindex="0"
|
||||
@click="isUser ? score('down') : null"
|
||||
@keypress.enter="isUser ? score('down') : null"
|
||||
@click="score('down')"
|
||||
@keypress.enter="score('down')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-if="showTaskLockIcon"
|
||||
class="svg-icon color lock"
|
||||
v-html="icons.lock"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon mb-1"
|
||||
v-html="icons.gold"
|
||||
></div>
|
||||
<div class="small-text">
|
||||
@@ -405,10 +402,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<approval-footer
|
||||
v-if="task.group.id"
|
||||
v-if="task.group.id && !isOpenTask"
|
||||
:task="task"
|
||||
:group="group"
|
||||
@claimRewards="score('up')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,7 +423,7 @@
|
||||
border: $purple-400 solid 1px;
|
||||
|
||||
:not(task-best-control-inner-habit) { // round icon
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,11 +445,11 @@
|
||||
margin-bottom: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&:hover:not(.task-not-editable),
|
||||
&:focus-within:not(.task-not-editable) {
|
||||
&:hover:not(.task-not-editable.task-not-scoreable),
|
||||
&:focus-within:not(.task-not-editable.task-not-scoreable) {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
z-index: 11;
|
||||
}
|
||||
@@ -469,10 +465,10 @@
|
||||
}
|
||||
|
||||
.task.groupTask {
|
||||
&:hover:not(.task-not-editable),
|
||||
&:focus-within:not(.task-not-editable) {
|
||||
&:hover:not(.task-not-editable.task-not-scoreable),
|
||||
&:focus-within:not(.task-not-editable.task-not-scoreable) {
|
||||
border: $purple-400 solid 1px;
|
||||
border-radius: 3px;
|
||||
border-radius: 5px;
|
||||
margin: -1px; // to counter the border width
|
||||
margin-bottom: 1px;
|
||||
transition: none; // with transition, the border color switches from black to $purple-400
|
||||
@@ -495,6 +491,7 @@
|
||||
}
|
||||
|
||||
&.has-notes {
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -508,8 +505,6 @@
|
||||
}
|
||||
|
||||
.task-clickable-area {
|
||||
padding: 7px 8px;
|
||||
padding-bottom: 0px;
|
||||
border: transparent solid 1px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -518,7 +513,7 @@
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
border: $purple-400 solid 1px;
|
||||
}
|
||||
}
|
||||
@@ -575,7 +570,11 @@
|
||||
}
|
||||
|
||||
.task-dropdown {
|
||||
max-height: 18px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-dropdown ::v-deep .dropdown-menu {
|
||||
@@ -632,8 +631,8 @@
|
||||
}
|
||||
|
||||
&.reward-content {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,8 +795,8 @@
|
||||
}
|
||||
}
|
||||
.left-control {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
min-height: 60px;
|
||||
border: 1px solid transparent;
|
||||
border-right: none;
|
||||
@@ -809,15 +808,15 @@
|
||||
.task:not(.type_habit) {
|
||||
.left-control {
|
||||
& + .task-content {
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-control {
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
min-height: 56px;
|
||||
border: 1px solid transparent;
|
||||
border-left: none;
|
||||
@@ -853,8 +852,12 @@
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lock {
|
||||
color: $gray-200;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
margin-top: 4px;
|
||||
font-style: initial;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -920,20 +923,19 @@ import lockIcon from '@/assets/svg/lock.svg';
|
||||
import menuIcon from '@/assets/svg/menu.svg';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import scoreTask from '@/mixins/scoreTask';
|
||||
import approvalHeader from './approvalHeader';
|
||||
import sync from '@/mixins/sync';
|
||||
import approvalFooter from './approvalFooter';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
approvalFooter,
|
||||
approvalHeader,
|
||||
MenuDropdown,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [scoreTask],
|
||||
mixins: [scoreTask, sync],
|
||||
// @TODO: maybe we should store the group on state?
|
||||
props: {
|
||||
task: {},
|
||||
@@ -1064,11 +1066,45 @@ export default {
|
||||
},
|
||||
teamManagerAccess () {
|
||||
if (!this.isGroupTask || !this.group) return true;
|
||||
if (!this.group.leader && !this.group.managers) return false;
|
||||
return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]);
|
||||
},
|
||||
displayNotes () {
|
||||
if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes;
|
||||
return this.task.notes;
|
||||
isOpenTask () {
|
||||
if (!this.isGroupTask) return false;
|
||||
if (this.task?.group?.assignedUsers?.length > 0) return false;
|
||||
return true;
|
||||
},
|
||||
showCheckIcon () {
|
||||
if (this.isGroupTask && this.task.group.assignedUsersDetail
|
||||
&& this.task.group.assignedUsersDetail[this.user._id]) {
|
||||
return this.task.group.assignedUsersDetail[this.user._id].completed;
|
||||
}
|
||||
return this.task.completed;
|
||||
},
|
||||
showTaskLockIcon () {
|
||||
if (this.isUser) return false;
|
||||
if (this.isGroupTask) {
|
||||
if (this.task.completed) {
|
||||
if (this.task.group.assignedUsersDetail
|
||||
&& this.task.group.assignedUsersDetail[this.user._id]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.task.group.completedBy.userId === this.user._id) return false;
|
||||
if (this.teamManagerAccess) {
|
||||
if (!this.task.group.assignedUsers || this.task.group.assignedUsers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (this.task.group.assignedUsers.length === 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (this.isOpenTask) return false;
|
||||
if (this.task.group.assignedUsersDetail[this.user._id]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -1098,7 +1134,7 @@ export default {
|
||||
return this.task.date && this.$t('dueIn', { dueIn });
|
||||
},
|
||||
edit (e, task) {
|
||||
if (this.isRunningYesterdailies || !this.showEdit) return;
|
||||
if (this.isRunningYesterdailies) return;
|
||||
const target = e.target || e.srcElement;
|
||||
|
||||
/*
|
||||
@@ -1116,8 +1152,11 @@ export default {
|
||||
const isEditAction = this.$refs.editTaskItem && this.$refs.editTaskItem.contains(target);
|
||||
|
||||
if (isDropdown && !isEditAction) return;
|
||||
if (this.$store.state.spellOptions.castingSpell) return;
|
||||
|
||||
if (!this.$store.state.spellOptions.castingSpell) {
|
||||
if (!this.showEdit) {
|
||||
this.$emit('taskSummary', task);
|
||||
} else {
|
||||
this.$emit('editTask', task);
|
||||
}
|
||||
},
|
||||
@@ -1137,6 +1176,22 @@ export default {
|
||||
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
|
||||
},
|
||||
async score (direction) {
|
||||
if (this.showTaskLockIcon) return;
|
||||
if (this.task.type === 'habit' && !this.task[direction]) return;
|
||||
if (
|
||||
this.isGroupTask && direction === 'down'
|
||||
&& ['todo', 'daily'].indexOf(this.task.type) !== -1
|
||||
&& !((this.task.group.completedBy && this.task.group.completedBy.userId === this.user._id)
|
||||
|| (this.task.group.assignedUsersDetail
|
||||
&& this.task.group.assignedUsersDetail[this.user._id]))
|
||||
) {
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.task._id,
|
||||
userId: this.task.group.assignedUsers[0] || this.task.group.completedBy.userId,
|
||||
});
|
||||
this.task.completed = false;
|
||||
return;
|
||||
}
|
||||
if (this.isYesterdaily === true) {
|
||||
await this.beforeTaskScore(this.task);
|
||||
this.task.completed = !this.task.completed;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
size="sm"
|
||||
:hide-footer="true"
|
||||
@hidden="onClose()"
|
||||
@show="handleOpen()"
|
||||
@show="syncTask()"
|
||||
@shown="focusInput()"
|
||||
>
|
||||
<div
|
||||
@@ -22,7 +22,9 @@
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div class="ml-auto d-flex align-items-center">
|
||||
<div
|
||||
class="ml-auto d-flex align-items-center"
|
||||
>
|
||||
<button
|
||||
class="cancel-task-btn mr-3"
|
||||
:class="cssClass('headings')"
|
||||
@@ -55,7 +57,7 @@
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="`${$t('text')}*`"
|
||||
/>
|
||||
<input
|
||||
@@ -66,28 +68,29 @@
|
||||
type="text"
|
||||
required="required"
|
||||
spellcheck="true"
|
||||
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:disabled="challengeAccessRequired"
|
||||
:placeholder="$t('addATitle')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUserTask || isChallengeTask || isOriginalChallengeTask"
|
||||
class="form-group mb-0"
|
||||
>
|
||||
<label
|
||||
class="d-flex align-items-center justify-content-between mb-1"
|
||||
>
|
||||
<span
|
||||
:class="cssClass('headings')"
|
||||
>{{ $t('notes') }}</span>
|
||||
<small>
|
||||
<div class="d-flex">
|
||||
<lockable-label
|
||||
class="mr-auto"
|
||||
:class-override="cssClass('headings')"
|
||||
:text="`${$t('notes')}`"
|
||||
/>
|
||||
<small
|
||||
class="my-1"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet"
|
||||
:class="cssClass('headings')"
|
||||
>{{ $t('markdownHelpLink') }}</a>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="task.notes"
|
||||
class="form-control input-notes"
|
||||
@@ -95,49 +98,6 @@
|
||||
:placeholder="$t('addNotes')"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="showManagerNotes"
|
||||
class="form-group mb-0 mt-3"
|
||||
>
|
||||
<lockable-label
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="groupAccessRequiredAndOnPersonalPage"
|
||||
:text="$t('managerNotes')"
|
||||
/>
|
||||
<textarea
|
||||
v-model="managerNotes"
|
||||
class="form-control input-notes"
|
||||
:class="cssClass('input')"
|
||||
:placeholder="$t('addNotes')"
|
||||
:disabled="groupAccessRequiredAndOnPersonalPage"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.group && task.group.assignedDate && !task.group.assigningUsername"
|
||||
class="mt-3 mb-n2"
|
||||
:class="cssClass('headings')"
|
||||
v-html="$t('assignedDateOnly', {
|
||||
date: formattedDate(task.group.assignedDate),
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.group && task.group.assignedDate && task.group.assigningUsername"
|
||||
class="mt-3 mb-n2"
|
||||
:class="cssClass('headings')"
|
||||
v-html="$t('assignedDateAndUser', {
|
||||
username: task.group.assigningUsername,
|
||||
date: formattedDate(task.group.assignedDate),
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task && groupAccessRequiredAndOnPersonalPage
|
||||
&& (task.type === 'daily' || task.type === 'todo')"
|
||||
class="summary-sentence py-3 px-4"
|
||||
v-html="summarySentence"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="task"
|
||||
@@ -181,8 +141,6 @@
|
||||
>
|
||||
<checklist
|
||||
:items.sync="task.checklist"
|
||||
:disable-items="groupAccessRequiredAndOnPersonalPage"
|
||||
:disable-drag="groupAccessRequiredAndOnPersonalPage"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -194,7 +152,7 @@
|
||||
class="habit-option-container no-transition
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
:class="!task.up ? cssClass('habit-control-disabled') : ''"
|
||||
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:disabled="challengeAccessRequired"
|
||||
@click="toggleUpDirection()"
|
||||
>
|
||||
<div
|
||||
@@ -220,7 +178,7 @@
|
||||
class="habit-option-container no-transition
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
:class="!task.down ? cssClass('habit-control-disabled') : ''"
|
||||
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:disabled="challengeAccessRequired"
|
||||
@click="toggleDownDirection()"
|
||||
>
|
||||
<div
|
||||
@@ -243,11 +201,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<template
|
||||
v-if="task.type !== 'reward' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type !== 'reward'"
|
||||
>
|
||||
<div class="d-flex mt-3">
|
||||
<lockable-label
|
||||
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="$t('difficulty')"
|
||||
/>
|
||||
<div
|
||||
@@ -258,13 +216,12 @@
|
||||
</div>
|
||||
<select-difficulty
|
||||
:value="task.priority"
|
||||
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
|
||||
:disabled="challengeAccessRequired"
|
||||
@select="setDifficulty($event)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-if="task.type === 'todo' && !groupAccessRequiredAndOnPersonalPage
|
||||
&& (!challengeAccessRequired || task.date)"
|
||||
v-if="task.type === 'todo' && (!challengeAccessRequired || task.date)"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -281,7 +238,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -297,7 +254,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -314,7 +271,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -344,8 +301,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && task.frequency === 'weekly'
|
||||
&& !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="task.type === 'daily' && task.frequency === 'weekly'"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -405,7 +361,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isUserTask"
|
||||
v-if="!groupId"
|
||||
class="tags-select option mt-3"
|
||||
>
|
||||
<div class="tags-inline form-group row">
|
||||
@@ -428,16 +384,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'habit'"
|
||||
v-if="task.type === 'habit' && !groupId"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
:locked="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="$t('resetCounter')"
|
||||
/>
|
||||
<select-translated-array
|
||||
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
|
||||
:disabled="challengeAccessRequired"
|
||||
:items="['daily', 'weekly', 'monthly']"
|
||||
:value="task.frequency"
|
||||
@select="task.frequency = $event"
|
||||
@@ -448,25 +404,18 @@
|
||||
v-if="groupId"
|
||||
class="option group-options mt-3"
|
||||
>
|
||||
<div
|
||||
v-if="task.type === 'todo'"
|
||||
class="form-group"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
class="mb-1"
|
||||
>{{ $t('sharedCompletion') }}</label>
|
||||
<select-translated-array
|
||||
:items="['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']"
|
||||
:value="sharedCompletion"
|
||||
@select="sharedCompletion = $event"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group row mt-3 mb-3">
|
||||
<label
|
||||
v-once
|
||||
class="col-12 mb-1"
|
||||
>{{ $t('assignedTo') }}</label>
|
||||
class="col-10 mb-1"
|
||||
>{{ $t('assignTo') }}</label>
|
||||
<a
|
||||
v-if="assignedMembers.length > 0"
|
||||
class="col-2 text-right mt-1"
|
||||
@click="clearAssignments"
|
||||
>
|
||||
{{ $t('clear') }}
|
||||
</a>
|
||||
<div class="col-12">
|
||||
<select-multi
|
||||
ref="assignMembers"
|
||||
@@ -479,17 +428,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group flex-group mt-3 mb-4">
|
||||
<label
|
||||
v-once
|
||||
class="mb-0 flex"
|
||||
>{{ $t('approvalRequired') }}</label>
|
||||
<toggle-switch
|
||||
class="d-inline-block"
|
||||
:checked="requiresApproval"
|
||||
@change="updateRequiresApproval"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="advancedSettingsAvailable"
|
||||
@@ -605,9 +543,7 @@
|
||||
</b-collapse>
|
||||
</div>
|
||||
<div
|
||||
v-if="purpose !== 'create'
|
||||
&& !challengeAccessRequired
|
||||
&& !groupAccessRequiredAndOnPersonalPage"
|
||||
v-if="purpose !== 'create' && !challengeAccessRequired"
|
||||
class="d-flex justify-content-center mt-4 mb-4"
|
||||
>
|
||||
<button
|
||||
@@ -654,6 +590,12 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#task-modal {
|
||||
a:not(.dropdown-item) {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.modal-dialog.modal-sm {
|
||||
max-width: 448px;
|
||||
}
|
||||
@@ -686,9 +628,6 @@
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
&:not(:host-context(.tags-popup)) {
|
||||
border: none;
|
||||
}
|
||||
transition-property: border-color, box-shadow, color, background;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
&:focus:not(:disabled), &:active:not(:disabled), &:hover:not(:disabled) {
|
||||
@@ -1026,11 +965,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.summary-sentence {
|
||||
background-color: $gray-700;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
@@ -1051,12 +985,8 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import clone from 'lodash/clone';
|
||||
import keys from 'lodash/keys';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import moment from 'moment';
|
||||
import Datepicker from '@/components/ui/datepicker';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
import toggleCheckbox from '@/components/ui/toggleCheckbox';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { mapGetters, mapActions, mapState } from '@/libs/store';
|
||||
@@ -1066,6 +996,8 @@ import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty
|
||||
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
|
||||
import syncTask from '../../mixins/syncTask';
|
||||
|
||||
import informationIcon from '@/assets/svg/information.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import negativeIcon from '@/assets/svg/negative.svg';
|
||||
@@ -1076,11 +1008,11 @@ import chevronIcon from '@/assets/svg/chevron.svg';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
||||
import gripIcon from '@/assets/svg/grip.svg';
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SelectMulti,
|
||||
Datepicker,
|
||||
toggleSwitch,
|
||||
checklist,
|
||||
selectDifficulty,
|
||||
selectTranslatedArray,
|
||||
@@ -1090,6 +1022,7 @@ export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [syncTask],
|
||||
// purpose is either create or edit, task is the task created or edited
|
||||
props: ['task', 'purpose', 'challengeId', 'groupId'],
|
||||
data () {
|
||||
@@ -1107,9 +1040,6 @@ export default {
|
||||
calendar: calendarIcon,
|
||||
grip: gripIcon,
|
||||
}),
|
||||
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
|
||||
sharedCompletion: 'singleCompletion',
|
||||
managerNotes: '',
|
||||
members: [],
|
||||
membersNameAndId: [],
|
||||
memberNamesById: {},
|
||||
@@ -1123,15 +1053,6 @@ export default {
|
||||
per: 'perception',
|
||||
},
|
||||
calendarHighlights: { dates: [new Date()] },
|
||||
expandDayString: {
|
||||
su: 'Sunday',
|
||||
m: 'Monday',
|
||||
t: 'Tuesday',
|
||||
w: 'Wednesday',
|
||||
th: 'Thursday',
|
||||
f: 'Friday',
|
||||
s: 'Saturday',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1150,7 +1071,6 @@ export default {
|
||||
|| this.task.type === 'todo'
|
||||
|| this.purpose === 'create'
|
||||
|| !this.isUserTask
|
||||
|| this.groupAccessRequiredAndOnPersonalPage
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -1164,29 +1084,17 @@ export default {
|
||||
|
||||
return true;
|
||||
},
|
||||
groupAccessRequiredAndOnPersonalPage () {
|
||||
if (!this.groupId && this.task.group && this.task.group.id) return true;
|
||||
return false;
|
||||
},
|
||||
checklistEnabled () {
|
||||
return ['daily', 'todo'].indexOf(this.task.type) > -1
|
||||
&& !this.isOriginalChallengeTask
|
||||
&& (!this.groupAccessRequiredAndOnPersonalPage || this.checklist.length > 0);
|
||||
},
|
||||
showManagerNotes () {
|
||||
return Boolean(this.task.group && this.task.group.managerNotes)
|
||||
|| (
|
||||
!this.groupAccessRequiredAndOnPersonalPage && this.managers.indexOf(this.user._id) !== -1
|
||||
);
|
||||
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
|
||||
},
|
||||
isChallengeTask () {
|
||||
return Boolean(this.task.challenge && this.task.challenge.id);
|
||||
},
|
||||
onUserPage () {
|
||||
isUserTask () {
|
||||
return !this.challengeId && !this.groupId;
|
||||
},
|
||||
challengeAccessRequired () {
|
||||
return this.onUserPage && this.isChallengeTask;
|
||||
return this.isUserTask && this.isChallengeTask;
|
||||
},
|
||||
isOriginalChallengeTask () {
|
||||
const isUserChallenge = Boolean(this.task.userId);
|
||||
@@ -1203,9 +1111,6 @@ export default {
|
||||
const type = this.$t(this.task.type);
|
||||
return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', { type });
|
||||
},
|
||||
isUserTask () {
|
||||
return !this.challengeId && !this.groupId;
|
||||
},
|
||||
repeatSuffix () {
|
||||
const { task } = this;
|
||||
|
||||
@@ -1240,23 +1145,6 @@ export default {
|
||||
selectedTags () {
|
||||
return this.getTagsFor(this.task);
|
||||
},
|
||||
summarySentence () {
|
||||
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)}
|
||||
task that will repeat
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
|
||||
}
|
||||
if (this.task.type === 'daily') {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)}
|
||||
task that repeats
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
|
||||
}
|
||||
if (this.task.date) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
|
||||
}
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
task () {
|
||||
@@ -1284,47 +1172,6 @@ export default {
|
||||
createTask: 'tasks:create',
|
||||
createTag: 'tags:createTag',
|
||||
}),
|
||||
async syncTask () {
|
||||
if (this.task?.group?.managerNotes) {
|
||||
this.managerNotes = this.task.group.managerNotes;
|
||||
}
|
||||
if (this.groupId && this.task.group?.approval) {
|
||||
this.requiresApproval = this.task.group.approval.required;
|
||||
}
|
||||
if (this.task?.group?.sharedCompletion) {
|
||||
this.sharedCompletion = this.task.group.sharedCompletion;
|
||||
} else if (this.task.group) {
|
||||
this.sharedCompletion = 'singleCompletion';
|
||||
}
|
||||
|
||||
if (this.groupId) {
|
||||
const members = await this.$store.dispatch('members:getGroupMembers', {
|
||||
groupId: this.groupId,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
this.members = members;
|
||||
this.membersNameAndId = [];
|
||||
this.members.forEach(member => {
|
||||
this.membersNameAndId.push({
|
||||
id: member._id,
|
||||
name: member.profile.name,
|
||||
addlText: `@${member.auth.local.username}`,
|
||||
});
|
||||
this.memberNamesById[member._id] = member.profile.name;
|
||||
});
|
||||
this.assignedMembers = [];
|
||||
if (this.task.group && this.task.group.assignedUsers) {
|
||||
this.assignedMembers = this.task.group.assignedUsers;
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: This whole component is mutating a prop
|
||||
// and that causes issues. We need to not copy the prop similar to group modals
|
||||
if (this.task) this.checklist = clone(this.task.checklist);
|
||||
},
|
||||
async handleOpen () {
|
||||
this.syncTask();
|
||||
},
|
||||
cssClass (suffix) {
|
||||
if (!this.task) {
|
||||
return '';
|
||||
@@ -1350,104 +1197,6 @@ export default {
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
},
|
||||
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
|
||||
let activeDays;
|
||||
const dayStringArray = [];
|
||||
switch (frequency) {
|
||||
case 'weekly':
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
if (activeDays.length === 0) return ' on <strong>no days</strong>';
|
||||
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
|
||||
dayStringArray.push(' on <strong>');
|
||||
activeDays.forEach((value, index) => {
|
||||
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(` ${this.expandDayString[value]}`);
|
||||
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
break;
|
||||
case 'monthly':
|
||||
dayStringArray.push(' on <strong>the ');
|
||||
if (daysOfMonth.length > 0) {
|
||||
daysOfMonth.forEach((value, index) => {
|
||||
const stringDay = String(value);
|
||||
const stringFinalDigit = stringDay.slice(-1);
|
||||
let ordinalSuffix = 'th';
|
||||
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
|
||||
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
|
||||
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
|
||||
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
|
||||
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
} else if (weeksOfMonth.length > 0) {
|
||||
switch (weeksOfMonth[0]) {
|
||||
case 0:
|
||||
dayStringArray.push('first');
|
||||
break;
|
||||
case 1:
|
||||
dayStringArray.push('second');
|
||||
break;
|
||||
case 2:
|
||||
dayStringArray.push('third');
|
||||
break;
|
||||
case 3:
|
||||
dayStringArray.push('fourth');
|
||||
break;
|
||||
case 4:
|
||||
dayStringArray.push('fifth');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
|
||||
}
|
||||
break;
|
||||
case 'yearly':
|
||||
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
return dayStringArray.join('');
|
||||
},
|
||||
formattedDifficulty (priority) {
|
||||
switch (priority) {
|
||||
case 0.1:
|
||||
return 'a <strong>trivial</strong>';
|
||||
case 1:
|
||||
return 'an <strong>easy</strong>';
|
||||
case 1.5:
|
||||
return 'a <strong>medium</strong>';
|
||||
case 2:
|
||||
return 'a <strong>hard</strong>';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
formattedRepeatInterval (frequency, everyX) {
|
||||
const numericX = Number(everyX);
|
||||
switch (frequency) {
|
||||
case 'daily':
|
||||
if (numericX === 1) return '<strong>every day</strong>';
|
||||
if (numericX === 2) return '<strong>every other day</strong>';
|
||||
return `<strong>every ${numericX} days</strong>`;
|
||||
case 'weekly':
|
||||
if (numericX === 1) return '<strong>every week</strong>';
|
||||
if (numericX === 2) return '<strong>every other week</strong>';
|
||||
return `<strong>every ${numericX} weeks</strong>`;
|
||||
case 'monthly':
|
||||
if (numericX === 1) return '<strong>every month</strong>';
|
||||
if (numericX === 2) return '<strong>every other month</strong>';
|
||||
return `<strong>every ${numericX} months</strong>`;
|
||||
case 'yearly':
|
||||
if (numericX === 1) return '<strong>every year</strong>';
|
||||
return `<strong>every ${everyX} years</strong>`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
calculateMonthlyRepeatDays (newRepeatsOn) {
|
||||
if (!this.task) return;
|
||||
const { task } = this;
|
||||
@@ -1475,16 +1224,6 @@ export default {
|
||||
if (!this.canSave) return;
|
||||
if (this.newChecklistItem) this.addChecklistItem();
|
||||
|
||||
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
|
||||
if (this.groupId) {
|
||||
this.task.requiresApproval = this.requiresApproval;
|
||||
this.task.group.approval.required = this.requiresApproval;
|
||||
this.task.sharedCompletion = this.sharedCompletion;
|
||||
this.task.group.sharedCompletion = this.sharedCompletion;
|
||||
this.task.managerNotes = this.managerNotes;
|
||||
this.task.group.managerNotes = this.managerNotes;
|
||||
}
|
||||
|
||||
if (this.task.type === 'reward' && this.task.value === '') {
|
||||
this.task.value = 0;
|
||||
}
|
||||
@@ -1503,12 +1242,18 @@ export default {
|
||||
tasks: [this.task],
|
||||
});
|
||||
Object.assign(this.task, response);
|
||||
const promises = this.assignedMembers.map(memberId => this.$store.dispatch('tasks:assignTask', {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: memberId,
|
||||
}));
|
||||
Promise.all(promises);
|
||||
this.task.group.assignedUsers = this.assignedMembers;
|
||||
assignedUserIds: this.assignedMembers,
|
||||
});
|
||||
this.assignedMembers.forEach(memberId => {
|
||||
if (!this.task.assignedUsersDetail) this.task.assignedUsersDetail = {};
|
||||
this.task.assignedUsersDetail[memberId] = {
|
||||
assignedDate: new Date(),
|
||||
assigningUsername: this.user.auth.local.username,
|
||||
completed: false,
|
||||
};
|
||||
});
|
||||
this.$emit('taskCreated', this.task);
|
||||
} else {
|
||||
this.createTask(this.task);
|
||||
@@ -1530,22 +1275,14 @@ export default {
|
||||
this.$root.$emit('bv::hide::modal', 'task-modal');
|
||||
},
|
||||
onClose () {
|
||||
if (this.task.group && this.task.group.managerNotes) this.managerNotes = null;
|
||||
this.newChecklistItem = '';
|
||||
this.$emit('cancel');
|
||||
},
|
||||
updateRequiresApproval (newValue) {
|
||||
let truthy = true;
|
||||
if (!newValue) truthy = false; // This return undefined instad of false
|
||||
this.requiresApproval = truthy;
|
||||
},
|
||||
async toggleAssignment (memberId) {
|
||||
if (this.purpose === 'create') {
|
||||
return;
|
||||
}
|
||||
|
||||
const assignedIndex = this.assignedMembers.indexOf(memberId);
|
||||
|
||||
if (assignedIndex === -1) {
|
||||
await this.$store.dispatch('tasks:unassignTask', {
|
||||
taskId: this.task._id,
|
||||
@@ -1554,10 +1291,21 @@ export default {
|
||||
} else {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: memberId,
|
||||
assignedUserIds: [memberId],
|
||||
});
|
||||
}
|
||||
},
|
||||
async clearAssignments () {
|
||||
if (this.purpose === 'edit') {
|
||||
for (const assignedMember of this.assignedMembers) {
|
||||
await this.$store.dispatch('tasks:unassignTask', { // eslint-disable-line no-await-in-loop
|
||||
taskId: this.task._id,
|
||||
userId: assignedMember,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.assignedMembers = [];
|
||||
},
|
||||
focusInput () {
|
||||
this.$refs.inputToFocus.focus();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="task-summary"
|
||||
:hide-footer="true"
|
||||
@hidden="$emit('cancel')"
|
||||
>
|
||||
<div
|
||||
v-if="task"
|
||||
slot="modal-header"
|
||||
class="task-modal-header px-4 d-flex align-items-center"
|
||||
:class="cssClass('bg')"
|
||||
>
|
||||
<h2
|
||||
class="my-auto"
|
||||
:class="cssClass('headings')"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div
|
||||
class="svg-icon color close-x ml-auto my-auto"
|
||||
:class="cssClass('headings')"
|
||||
aria-hidden="true"
|
||||
tabindex="0"
|
||||
@click="cancel()"
|
||||
@keypress.enter="cancel()"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task"
|
||||
class="task-modal-content pt-3 px-4 pb-4"
|
||||
>
|
||||
<div class="summary-block">
|
||||
<h3> {{ $t('title') }} </h3>
|
||||
<p> {{ task.text }} </p>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.notes"
|
||||
class="summary-block"
|
||||
>
|
||||
<h3> {{ $t('notes') }} </h3>
|
||||
<p> {{ task.notes }} </p>
|
||||
</div>
|
||||
<div class="summary-block">
|
||||
<h3> {{ $t('description') }} </h3>
|
||||
<p v-html="summarySentence" ></p>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.checklist && task.checklist.length > 0"
|
||||
class="summary-block"
|
||||
>
|
||||
<checklist
|
||||
:items.sync="task.checklist"
|
||||
:disableDrag="true"
|
||||
:disableEdit="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="summary-block"
|
||||
v-if="assignedUsernames.length > 0"
|
||||
>
|
||||
<h3> {{ $t('assignedTo') }} </h3>
|
||||
<div
|
||||
class="d-flex flex-wrap"
|
||||
>
|
||||
<span
|
||||
v-for="member in assignedUsernames"
|
||||
:key="member"
|
||||
class="assigned-member py-1 px-75 mb-1 mr-1"
|
||||
>
|
||||
@{{ member }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task && task.group && task.group.assignedUsersDetail
|
||||
&& task.group.assignedUsersDetail[user._id]"
|
||||
class="assignment-footer text-center py-2"
|
||||
v-html="$t('assignedDateAndUser', {
|
||||
username: task.group.assignedUsersDetail[user._id].assigningUsername,
|
||||
date: formattedDate(task.group.assignedUsersDetail[user._id].assignedDate),
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
#task-summary {
|
||||
overflow-y: hidden;
|
||||
|
||||
.modal-content {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
|
||||
}
|
||||
|
||||
.modal-header, .modal-body, .modal-footer {
|
||||
padding: 0px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
margin-top: 50vh;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.assigned-member {
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 100px;
|
||||
color: $gray-100;
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.assignment-footer {
|
||||
color: $gray-100;
|
||||
background-color: $gray-700;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.close-x {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-block:not(:last-of-type) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.task-modal-content {
|
||||
h3 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3, p {
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.task-modal-header {
|
||||
color: $white;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
|
||||
h2 {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import keys from 'lodash/keys';
|
||||
import moment from 'moment';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
|
||||
import checklist from './modal-controls/checklist';
|
||||
import { mapGetters, mapState } from '@/libs/store';
|
||||
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
checklist,
|
||||
},
|
||||
props: ['task'],
|
||||
data () {
|
||||
return {
|
||||
expandDayString: {
|
||||
su: 'Sunday',
|
||||
m: 'Monday',
|
||||
t: 'Tuesday',
|
||||
w: 'Wednesday',
|
||||
th: 'Thursday',
|
||||
f: 'Friday',
|
||||
s: 'Saturday',
|
||||
},
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getTaskClasses: 'tasks:getTaskClasses',
|
||||
}),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
assignedUsernames () {
|
||||
if (!this.task.group || !this.task.group.assignedUsers
|
||||
|| !this.task.group.assignedUsersDetail) return [];
|
||||
const usernames = [];
|
||||
for (const user of this.task.group.assignedUsers) {
|
||||
usernames.push(this.task.group.assignedUsersDetail[user].assignedUsername);
|
||||
}
|
||||
return usernames;
|
||||
},
|
||||
summarySentence () {
|
||||
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
|
||||
}
|
||||
if (this.task.type === 'daily') {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
|
||||
}
|
||||
if (this.task.date) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
|
||||
}
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
|
||||
},
|
||||
title () {
|
||||
const type = this.$t(this.task.type);
|
||||
return this.$t('taskSummary', { type });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancel () {
|
||||
this.$root.$emit('bv::hide::modal', 'task-summary');
|
||||
},
|
||||
cssClass (suffix) {
|
||||
if (!this.task) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.getTaskClasses(this.task, `edit-modal-${suffix}`);
|
||||
},
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
},
|
||||
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
|
||||
let activeDays;
|
||||
const dayStringArray = [];
|
||||
switch (frequency) {
|
||||
case 'weekly':
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
if (activeDays.length === 0) return ' on <strong>no days</strong>';
|
||||
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
|
||||
dayStringArray.push(' on <strong>');
|
||||
activeDays.forEach((value, index) => {
|
||||
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(` ${this.expandDayString[value]}`);
|
||||
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
break;
|
||||
case 'monthly':
|
||||
dayStringArray.push(' on <strong>the ');
|
||||
if (daysOfMonth.length > 0) {
|
||||
daysOfMonth.forEach((value, index) => {
|
||||
const stringDay = String(value);
|
||||
const stringFinalDigit = stringDay.slice(-1);
|
||||
let ordinalSuffix = 'th';
|
||||
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
|
||||
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
|
||||
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
|
||||
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
|
||||
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
|
||||
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
} else if (weeksOfMonth.length > 0) {
|
||||
switch (weeksOfMonth[0]) {
|
||||
case 0:
|
||||
dayStringArray.push('first');
|
||||
break;
|
||||
case 1:
|
||||
dayStringArray.push('second');
|
||||
break;
|
||||
case 2:
|
||||
dayStringArray.push('third');
|
||||
break;
|
||||
case 3:
|
||||
dayStringArray.push('fourth');
|
||||
break;
|
||||
case 4:
|
||||
dayStringArray.push('fifth');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
|
||||
}
|
||||
break;
|
||||
case 'yearly':
|
||||
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
return dayStringArray.join('');
|
||||
},
|
||||
formattedDifficulty (priority) {
|
||||
switch (priority) {
|
||||
case 0.1:
|
||||
return 'a <strong>trivial</strong>';
|
||||
case 1:
|
||||
return 'an <strong>easy</strong>';
|
||||
case 1.5:
|
||||
return 'a <strong>medium</strong>';
|
||||
case 2:
|
||||
return 'a <strong>hard</strong>';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
formattedRepeatInterval (frequency, everyX) {
|
||||
const numericX = Number(everyX);
|
||||
switch (frequency) {
|
||||
case 'daily':
|
||||
if (numericX === 1) return '<strong>every day</strong>';
|
||||
if (numericX === 2) return '<strong>every other day</strong>';
|
||||
return `<strong>every ${numericX} days</strong>`;
|
||||
case 'weekly':
|
||||
if (numericX === 1) return '<strong>every week</strong>';
|
||||
if (numericX === 2) return '<strong>every other week</strong>';
|
||||
return `<strong>every ${numericX} weeks</strong>`;
|
||||
case 'monthly':
|
||||
if (numericX === 1) return '<strong>every month</strong>';
|
||||
if (numericX === 2) return '<strong>every other month</strong>';
|
||||
return `<strong>every ${numericX} months</strong>`;
|
||||
case 'yearly':
|
||||
if (numericX === 1) return '<strong>every year</strong>';
|
||||
return `<strong>every ${everyX} years</strong>`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="row user-tasks-page">
|
||||
<div
|
||||
class="row user-tasks-page"
|
||||
@click="openCreateBtn ? openCreateBtn = false : null"
|
||||
>
|
||||
<broken-task-modal />
|
||||
<task-modal
|
||||
ref="taskModal"
|
||||
@@ -7,6 +10,11 @@
|
||||
:purpose="creatingTask !== null ? 'create' : 'edit'"
|
||||
@cancel="cancelTaskModal()"
|
||||
/>
|
||||
<task-summary
|
||||
ref="taskSummary"
|
||||
:task="editingTask"
|
||||
@cancel="cancelTaskModal()"
|
||||
/>
|
||||
<div class="col-12">
|
||||
<div class="row tasks-navigation">
|
||||
<div class="col-12 col-md-4 offset-md-4">
|
||||
@@ -160,45 +168,43 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="create-task-area d-flex">
|
||||
<transition name="slide-tasks-btns">
|
||||
<div class="create-task-area">
|
||||
<div
|
||||
id="create-task-btn"
|
||||
class="btn btn-primary create-btn d-flex align-items-center"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click.stop.prevent="openCreateBtn = !openCreateBtn"
|
||||
@keypress.enter="openCreateBtn = !openCreateBtn"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
v-if="openCreateBtn"
|
||||
class="d-flex"
|
||||
class="svg-icon icon-10 color"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
|
||||
</div>
|
||||
<div
|
||||
v-if="openCreateBtn"
|
||||
class="dropdown"
|
||||
>
|
||||
<div
|
||||
v-for="type in columns"
|
||||
:key="type"
|
||||
@click="createTask(type)"
|
||||
class="dropdown-item d-flex px-2 py-1"
|
||||
>
|
||||
<div
|
||||
v-for="type in columns"
|
||||
:key="type"
|
||||
v-b-tooltip.hover.bottom="$t(type)"
|
||||
class="create-task-btn diamond-btn"
|
||||
@click="createTask(type)"
|
||||
>
|
||||
<div class="d-flex align-items-center justify-content-center task-icon">
|
||||
<div
|
||||
class="svg-icon"
|
||||
class="svg-icon m-auto"
|
||||
:class="`icon-${type}`"
|
||||
v-html="icons[type]"
|
||||
></div>
|
||||
</div>
|
||||
<div class="task-label ml-2">
|
||||
{{ $t(type) }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div
|
||||
id="create-task-btn"
|
||||
class="create-btn diamond-btn btn btn-success"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click="openCreateBtn = !openCreateBtn"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
</div>
|
||||
<b-tooltip
|
||||
v-if="!openCreateBtn"
|
||||
target="create-task-btn"
|
||||
placement="bottom"
|
||||
>
|
||||
{{ $t('addTask') }}
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row tasks-columns">
|
||||
@@ -211,6 +217,7 @@
|
||||
:search-text="searchTextThrottled"
|
||||
:selected-tags="selectedTags"
|
||||
@editTask="editTask"
|
||||
@taskSummary="taskSummary"
|
||||
@openBuyDialog="openBuyDialog($event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -345,7 +352,7 @@
|
||||
}
|
||||
|
||||
.create-task-area {
|
||||
top: -2.5rem;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.drag {
|
||||
@@ -381,6 +388,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import draggable from 'vuedraggable';
|
||||
import TaskColumn from './column';
|
||||
import TaskModal from './taskModal';
|
||||
import TaskSummary from './taskSummary';
|
||||
import spells from './spells';
|
||||
import markdown from '@/directives/markdown';
|
||||
|
||||
@@ -401,6 +409,7 @@ export default {
|
||||
components: {
|
||||
TaskColumn,
|
||||
TaskModal,
|
||||
TaskSummary,
|
||||
spells,
|
||||
brokenTaskModal,
|
||||
draggable,
|
||||
@@ -524,13 +533,19 @@ export default {
|
||||
};
|
||||
this.newTag = null;
|
||||
},
|
||||
// Need Vue.nextTick() otherwise the first time the modal is not rendered
|
||||
editTask (task) {
|
||||
this.editingTask = cloneDeep(task);
|
||||
// Necessary otherwise the first time the modal is not rendered
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
taskSummary (task) {
|
||||
this.editingTask = cloneDeep(task);
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-summary');
|
||||
});
|
||||
},
|
||||
createTask (type) {
|
||||
this.openCreateBtn = false;
|
||||
this.creatingTask = taskDefaults({ type, text: '' }, this.user);
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import scoreTask from '@/mixins/scoreTask';
|
||||
import sync from '@/mixins/sync';
|
||||
import Task from './task';
|
||||
import LoadingSpinner from '../ui/loadingSpinner';
|
||||
|
||||
@@ -92,7 +93,7 @@ export default {
|
||||
Task,
|
||||
LoadingSpinner,
|
||||
},
|
||||
mixins: [scoreTask],
|
||||
mixins: [scoreTask, sync],
|
||||
props: {
|
||||
yesterDailies: {
|
||||
type: Array,
|
||||
@@ -180,6 +181,7 @@ export default {
|
||||
|
||||
this.isLoading = false;
|
||||
this.$root.$emit('bv::hide::modal', 'yesterdaily');
|
||||
if (this.$route.fullPath.indexOf('task-information') !== -1) this.sync();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,6 +29,11 @@ export default [
|
||||
type: 'Staff',
|
||||
uuid: '61b2c855-0a30-444c-bcc6-1cac876460b0',
|
||||
},
|
||||
{
|
||||
name: 'heyeilatan',
|
||||
type: 'Staff',
|
||||
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
|
||||
},
|
||||
{
|
||||
name: 'Alys',
|
||||
type: 'Moderator',
|
||||
|
||||
@@ -17,8 +17,14 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
hideSet (set) {
|
||||
return moment(appearanceSets[set.key].availableUntil).isBefore(moment());
|
||||
hideSet (setKey) {
|
||||
if (appearanceSets[setKey].availableFrom) {
|
||||
return !moment().isBetween(
|
||||
appearanceSets[setKey].availableFrom,
|
||||
appearanceSets[setKey].availableUntil,
|
||||
);
|
||||
}
|
||||
return moment(appearanceSets[setKey].availableUntil).isBefore(moment());
|
||||
},
|
||||
mapKeysToFreeOption (key, type, subType) {
|
||||
const userPreference = subType
|
||||
@@ -43,8 +49,8 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
|
||||
const locked = !userPurchased || !userPurchased[key];
|
||||
let hide = false;
|
||||
|
||||
if (set && appearanceSets[set]) {
|
||||
if (locked) hide = moment(appearanceSets[set].availableUntil).isBefore(moment());
|
||||
if (set && appearanceSets[set] && locked) {
|
||||
hide = this.hideSet(set);
|
||||
}
|
||||
|
||||
option.gemLocked = locked;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
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',
|
||||
|
||||
@@ -1969,5 +1969,50 @@
|
||||
"weaponSpecialWinter2021MageText": "Kouzelný měsíční phaser",
|
||||
"weaponSpecialWinter2021MageNotes": "Tato mocná zbraň je rozhodně víc než jen phaser. Usměrni svou energii, zaměř se na průběh měsíce a studuj časoprostor. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Výzbroj z limitované edice pro zimu 2020-2021.",
|
||||
"weaponSpecialSpring2021MageNotes": "Hoď, poraž, šlapej, odpočívej! Pírko sviští časem, aby dirigovalo hudbu tvých kouzel. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Výzbroj z limitované edice pro jaro 2021.",
|
||||
"weaponSpecialSpring2021HealerNotes": "Kůra a listy této čerstvě uříznuté větve jsou známy pro jejich schopnost zmírnění bolesti. Nebo můžeš větev zasadit a dívat se, jak roste! Zvyšuje inteligenci o <%= int %>. Výzbroj z limitované edice pro jaro 2021."
|
||||
"weaponSpecialSpring2021HealerNotes": "Kůra a listy této čerstvě uříznuté větve jsou známy pro jejich schopnost zmírnění bolesti. Nebo můžeš větev zasadit a dívat se, jak roste! Zvyšuje inteligenci o <%= int %>. Výzbroj z limitované edice pro jaro 2021.",
|
||||
"weaponSpecialSummer2021RogueNotes": "Jakékoliv dravé monstrum jenž se opováží přiblížit pocítí bodnutí tvých ochranných přátel. Zvyšuje sílu o <%= str %>. Limitovaná edice letní výzbroj 2021.",
|
||||
"weaponSpecialSummer2021WarriorText": "Vodní čepel",
|
||||
"weaponSpecialSummer2022RogueNotes": "Pokud jsi v nouzi, neváhej ukázat tato hrůzostrašná klepeta! Zvyšuje sílu o <%= str %>. Limitovaná edice zimní výzbroj 2022.",
|
||||
"weaponSpecialSummer2022WarriorNotes": "Točí se to! Mění to směr! A přináší to bouři! Zvyšuje sílu o <%= str %>. Limitovaná edice letní výzbroj 2022.",
|
||||
"weaponSpecialSummer2022MageText": "Hůl Manty obrovské",
|
||||
"weaponSpecialSummer2022MageNotes": "Magicky vyčisti vodu před tebou jedním mávnutím touto hůlkou. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice 2022 letní výzbroj.",
|
||||
"weaponSpecialSummer2022HealerNotes": "Tyto bubliny uvolňují do vody léčivou magii s uspokojujícím praskáním. Zvyšuje inteligenci o <%= int %>. Limitovaná edice letní výzbroj 2022.",
|
||||
"weaponSpecialFall2021MageNotes": "Znalosti hledají znalosti. Tato hrůzostrašná ruka, vytvořená ze vzpomínek a tužeb, se snaží získat víc. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice podzimní výzbroj 2021.",
|
||||
"weaponSpecialSummer2022RogueText": "Krabí klepeto",
|
||||
"weaponSpecialWinter2022HealerNotes": "Dotkni se tímto vodním náčiním krku přátel a oni vyskočí ze židle! Pak se však budou cítit lépe. Doufejme. Zvyšuje inteligenci o <%= int %>. Limitovaná edice zimní výzbroj 2021-2022.",
|
||||
"weaponSpecialWinter2022RogueNotes": "Stříbrňáky a zlaťáky zloději milují, že? Tyto zbraně jsou tedy zcela namístě. Zvyšuje sílu o <%= str %>. Limitovaná edice zimní výzbroj 2021-2022.",
|
||||
"weaponSpecialSummer2022WarriorText": "Vířivá cyklóna",
|
||||
"weaponSpecialSummer2022HealerText": "Blahodárné bubliny",
|
||||
"weaponSpecialWinter2022MageNotes": "Bobule na této hůlce obsahují starodávné kouzlo, jenž lze ovládat v zimě. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice zimní výzbroj 2021-2022.",
|
||||
"weaponSpecialSummer2021WarriorNotes": "Tato třpytivá čepel dokáže plynout jako voda, nicméně dokáže i proříznout do jádra těch nejzáludnějších problémů. Zvyšuje sílu o <%= str %>. Limitovaná edice letní výzbroj 2021.",
|
||||
"weaponSpecialSummer2021MageText": "Nautiloidní hůl",
|
||||
"weaponSpecialSummer2021MageNotes": "Nezáleží zda tvé magické ambice sahají do nesmírné hloubky nebo zda se chceš pouze nořit v mělčinách magie. Tento zářivý nástroj ti v obou případech dobře poslouží. Zvyšuje inteligenci o <%= int %> a vnímavost o <%= per %>. Limitovaná edice letní výzbroj 2021.",
|
||||
"weaponSpecialSummer2021HealerText": "Kukuřičná hůl",
|
||||
"weaponSpecialWinter2022RogueText": "Ohňostroj padající hvězdy",
|
||||
"weaponSpecialWinter2022WarriorText": "Meč z cukrové třtiny",
|
||||
"weaponSpecialWinter2022WarriorNotes": "Kolik líznutí je potřeba k nabroušení této cukrové třtiny do perfektního meče? Zvyšuje sílu o <%= str %>. Limitovaná edice zimní výzbroj 2021-2022.",
|
||||
"weaponSpecialWinter2022MageText": "Hůl z granátového jablka",
|
||||
"weaponSpecialWinter2022HealerText": "Hůl z ledového krystalu",
|
||||
"weaponSpecialFall2021RogueText": "Kapající sliz",
|
||||
"weaponSpecialFall2021RogueNotes": "Do čeho jsi se to proboha dostal? Když se říká že zloději mají lepkavé prsty, tohle se tím nemyslí! Zvyšuje sílu o <%= str %>. Limitovaná edice podzimní výzbroj 2021.",
|
||||
"weaponSpecialFall2021WarriorText": "Sekera jezdce na koni",
|
||||
"weaponSpecialFall2021WarriorNotes": "Tato stylová jednočepelová sekera je ideální na sekání.. dýní! Zvyšuje sílu o <%= str %>. Limitovaná edice podzimní výzbroj 2021.",
|
||||
"weaponSpecialFall2021MageText": "Hůl čisté myšlenky",
|
||||
"weaponSpecialFall2021HealerText": "Hůl přivolávání",
|
||||
"weaponSpecialFall2021HealerNotes": "Využij tuto hůl k přivolání léčivých plamenů a duchů, jenž ti pomohou. Zvyšuje inteligenci o <%= int %>. Limitovaná edice podzimní výbava 2021.",
|
||||
"weaponSpecialSpring2022WarriorNotes": "Jejda! Hádám že ten vítr byl trošku silnější než jsi si myslel, že? Zvyšuje sílu o <%= str %>. Limitovaná edice jarní výzbroj 2022.",
|
||||
"weaponSpecialSpring2022MageText": "Zlaticová hůl",
|
||||
"weaponSpecialSpring2022MageNotes": "Tyto zářivě žluté zvonky jsou připraveny nasměrovat tvoji mocnou jarní magii. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice jarní výzbroj 2022.",
|
||||
"weaponSpecialSpring2022HealerNotes": "Použij tuto hůlku k využití léčivých vlastností periodotu, ať už to má přinést klid, pozitivitu nebo laskavost. Zvyšuje inteligenci o <%= int %>. Limitovaná edice jarní výzbroj 2022.",
|
||||
"weaponSpecialSpring2022RogueText": "Obrovský narozeninový puzet",
|
||||
"weaponSpecialSpring2022RogueNotes": "Lesk! Je tak lesklá a třpytivá a hezká a krásná a celá tvoje! Zvyšuje sílu o <%= str %>. Limitovaná edice jarní výzbroj 2022.",
|
||||
"weaponSpecialSpring2022WarriorText": "Deštník naruby",
|
||||
"weaponSpecialSpring2022HealerText": "Peridotová hůlka",
|
||||
"headSpecialNye2021Notes": "Obdržel jsi Absurdní party čepici! Nasaď si ji s pýchou zatímco odbíjí Nový rok. Nepřináší žádné výhody.",
|
||||
"headSpecialNye2021Text": "Absurdní party čepice",
|
||||
"weaponSpecialWinter2021HealerNotes": "Vyraž do bitev s fanfárou a závanem větru! Zvyšuje inteligenci o <%= int %>. Limitovaná edice zimní výzbroj 2020-2021.",
|
||||
"weaponMystery202102Notes": "Zářivý růžový drahokem v této hůlce dokáže šířit radost a přátelství všude možně! Nepřináší žádné výhody. Předmět pro předplatitele, únor 2021.",
|
||||
"weaponMystery202104Notes": "Tvoji nepřátele by si měli dávat pozor - máš totiž mocnou a pichlavou obranu! Nepřináší žádné výhody. Předmět pro předplatitele duben 2021.",
|
||||
"weaponMystery202102Text": "Okouzlující hůlka",
|
||||
"weaponMystery202104Text": "Hůl trnitého bodláku"
|
||||
}
|
||||
|
||||
@@ -118,11 +118,24 @@
|
||||
"achievementDomesticatedText": "Har udklækket følgende tæmmede kæledyr i alle standardfarver: Fritte, Marsvin, Hane, Flyvende Gris, Rotte, Kanin, Hest, og Ko!",
|
||||
"achievementDomesticated": "E-I-E-I-O",
|
||||
"achievementDomesticatedModalText": "Du har indsamlet alle tæmmede kæledyr!",
|
||||
"achievementZodiacZookeeperText": "Har udklækket alle kæledyr, der er et kinesisk stjernetegn: Rotte, Ko, Kanin, Slange, Hest, Får, Abe, Hane, Ulv, Tiger, Flyvende Gris, og Drage!",
|
||||
"achievementZodiacZookeeperText": "Har udklækket alle kæledyr, der er et kinesisk stjernetegn, i alle standardfarver: Rotte, Ko, Kanin, Slange, Hest, Får, Abe, Hane, Ulv, Tiger, Flyvende Gris, og Drage!",
|
||||
"achievementZodiacZookeeperModalText": "Du har samlet alle dyr, der er et stjernetegn!",
|
||||
"achievementShadyCustomerText": "Har samlet alle Skyggekæledyr.",
|
||||
"achievementShadeOfItAllText": "Har tæmmet alle Skyggeridedyr.",
|
||||
"achievementShadeOfItAllModalText": "Du har tæmmet alle Skyggeridedyr!",
|
||||
"achievementShadyCustomer": "Lyssky kunde",
|
||||
"achievementShadyCustomerModalText": "Du har indsamlet alle Skygge Dyr!"
|
||||
"achievementShadyCustomerModalText": "Du har indsamlet alle Skygge Dyr!",
|
||||
"achievementGroupsBeta2022ModalText": "Du og dine grupper hjalp Habitica ved at teste funktioner og komme med feedback!",
|
||||
"achievementGroupsBeta2022": "Interaktiv betatester",
|
||||
"achievementGroupsBeta2022Text": "Du og din gruppe kom med uvurderlig feedback under en test af Habitica.",
|
||||
"achievementWoodlandWizard": "Skovheks",
|
||||
"achievementWoodlandWizardText": "Har udklækket alle skovens dyr i alle standardfarver: Grævling, Bjørn, Hjort, Ræv, Frø, Pindsvin, Ugle, Snegl, Egern og Træling!",
|
||||
"achievementWoodlandWizardModalText": "Du har samlet alle skovkæledyr!",
|
||||
"achievementReptacularRumbleText": "Has udklækket alle reptilkæledyr i alle standardfarver: Alligator, Flyveøgle, Slange, Triceratops, Skildpadde, Tyrannosaurus Rex og Velociraptor!",
|
||||
"achievementReptacularRumbleModalText": "Du har samlet alle reptilkæledyr!",
|
||||
"achievementBirdsOfAFeather": "Én fjer, fem høns",
|
||||
"achievementBirdsOfAFeatherModalText": "Du har samlet alle de flyvende kæledyr!",
|
||||
"achievementBirdsOfAFeatherText": "Har udklækket alle flyvende kæledyr i alle standardfarver: Flyvende Gris, Ugle, Papegøje, Flyveøgle, Grif, Falk, Påfugl og Hane!",
|
||||
"achievementZodiacZookeeper": "Dyrekredsens dyretæmmer",
|
||||
"achievementShadeOfItAll": "Skyggen af det hele"
|
||||
}
|
||||
|
||||
@@ -492,5 +492,6 @@
|
||||
"backgroundParkWithStatueText": "Park med Statue",
|
||||
"backgroundDojoNotes": "Lær nye teknikker i en Dojo.",
|
||||
"backgroundDojoText": "Dojo",
|
||||
"backgrounds052019": "SET 60: Frigivet Maj 2019"
|
||||
"backgrounds052019": "SET 60: Frigivet Maj 2019",
|
||||
"hideLockedBackgrounds": "Skjul låste baggrunde"
|
||||
}
|
||||
|
||||
@@ -103,5 +103,6 @@
|
||||
"selectParticipant": "Vælg en deltager",
|
||||
"filters": "Filtre",
|
||||
"wonChallengeDesc": "<%= challengeName %> udvalgte dig som vinder! Din sejr er noteret i dine Præstationer.",
|
||||
"yourReward": "Din Belønning"
|
||||
"yourReward": "Din Belønning",
|
||||
"removeTasks": "Fjern opgaver"
|
||||
}
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
"tavernCommunityGuidelinesPlaceholder": "Venlig påmindelse: Det her er en chat for alle aldre, så hold venligst en passende tone og vær opmærksom på indholdet af dine beskeder! Læs Retningslinjer for fællesskabet i sidepanelet, hvis du har spørgsmål.",
|
||||
"lastUpdated": "Sidst opdateret:",
|
||||
"commGuideHeadingWelcome": "Velkommen til Habitica!",
|
||||
"commGuidePara001": "Vær hilset, eventyrer! Velkommen til Habitica, et land af produktivitet, sund levemåde, og til tider en enkelt hærgende grif. Vi er et muntert fællesskab fuld af hjælpsomme folk der støtter op om hinanden på deres vej til at forbedre sig selv. For at passe ind har du kun brug for en positiv attitude, respektfuld opførsel, og forståelse for at alle har forskellige evner of begrænsninger -- inklusiv dig! Habiticanere er tålmodige med hinanden og forsøger at hjælpe hvor de kan.",
|
||||
"commGuidePara002": "For at sikre at alle er trygge, glade, og produktive i vores fællesskab, har vi nogle retningslinjer. Vi har omhyggeligt konstrueret dem så de er så venlige og letlæselige som muligt. Brug venligt et øjeblik på at læse dem inden du begynder at chatte.",
|
||||
"commGuidePara003": "Disse regler gælder alle de sociale områder vi bruger, inklusiv (men ikke nødvendigvis begrænset til) Trello, GitHub, Weblate og Wikia (wiki'en). Sommetider vil der opstå uforudsete situationer, eksempelvis en ny kilde til konflikt eller en ondskabsfuld åndemaner. Når det sker, kan moderatorer reagere ved at ændre disse retningslinjer for at beskytte fællesskabet mod ny trusler. Frygt ikke: Du vil blive gjort opmærksom på ændringer i retningslinjerne via en påmindelse fra Bailey.",
|
||||
"commGuidePara001": "Vær hilset, eventyrer! Velkommen til Habitica, et land af produktivitet, sund levemåde, og til tider en enkelt hærgende grif. Vi er et muntert fællesskab fuld af hjælpsomme folk der støtter op om hinanden på deres vej til at forbedre sig selv. For at passe ind har du kun brug for en positiv attitude, respektfuld opførsel, og forståelse for at alle har forskellige evner of begrænsninger - inklusiv dig! Habiticanere er tålmodige med hinanden og forsøger at hjælpe hvor de kan.",
|
||||
"commGuidePara002": "For at sikre at alle er trygge, glade, og produktive i vores fællesskab, har vi nogle retningslinjer. Vi har omhyggeligt konstrueret dem så de er så venlige og letlæselige som muligt. Brug venligst et øjeblik på at læse dem inden du begynder at chatte.",
|
||||
"commGuidePara003": "Disse regler gælder alle de sociale områder vi bruger, inklusiv (men ikke nødvendigvis begrænset til) Trello, GitHub, Weblate og Habitica wiki'en på fandom.com. Efterhånden som Habitica vokser og forandres, kan det ske at vores regler bliver ændret. Når der er sket bemærkelsesværdige ændringer af retningslinjerne for fællesskabet, vil du kunne høre om det i en meddelelse fra Bailey og/eller på vores sociale medier!",
|
||||
"commGuideHeadingInteractions": "Interaktioner i Habitica",
|
||||
"commGuidePara015": "Habitica har to slags sociale rum: offentlige og private. Offentlige rum inkluderer Værtshuset, Offentlige klaner, GitHub, Trello og Wiki'en. Private rum er Private klaner, Holdchatten og Private beskeder. Alle display names skal følge retningslinjerne for offentlige rum. Gå til Bruger > Indstillinger > Side for at ændre dit brugernavn.",
|
||||
"commGuidePara016": "Når du navigerer rundt i de offentlige steder af Habitica, er der nogle generelle regler for at sørge for, at alle er sikre og glade. De burde være lette for eventyrere som dig!",
|
||||
"commGuideList02A": "<strong>Respekter hinanden</strong>. Vær høflig, venlig og hjælpsom. Husk: Habiticanere kommer fra alle baggrunde og har haft vildt skiftende oplevelser og erfaringer. Det er en del af det, der gør Habitica så cool! At bygge et fællesskab betyder at vi respekterer og fejrer vores forskelle såvel som vores ligheder. Her er nogle nemme måder at vise respekt for hinanden på:",
|
||||
"commGuideList02B": "<strong>Adlyd alt i <a href='/static/terms' target='_blank'>Vilkår og betingelser</a></strong>.",
|
||||
"commGuideList02C": "<strong>Læg ikke billeder eller tekst op, der er voldeligt, truende eller seksuelt eksplicit/antydende, eller som promoverer diskrimination, fordomme, racisme, sexisme, had, chikane eller skade mod et individ eller en gruppe</strong>. Ikke engang som en joke. Dette inkluderer skældsord såvel som udtalelser. Ikke alle har den samme form for humor, så noget du anser som en joke er muligvis sårende for en anden. Angrib jeres Daglige opgaver, ikke hinanden.",
|
||||
"commGuideList02D": "<strong>Hold diskussioner passende for alle aldre</strong>. Vi har mange unge Habiticanere der bruger hjemmesiden! Lad os ikke genere nogen uskyldige eller forhindre nogen Habiticanere i at opnår deres mål.",
|
||||
"commGuideList02E": "<strong>Undgå bandeord. Dette inkluderer mildere, religiøst baserede bandeord der kan være acceptable andre steder</strong>. Vi har folk fra alle religiøse og kulturelle baggrunde, og vi vil gerne sørge for at de alle føler sig trygge i offentlige rum. <strong>Hvis en moderator eller medarbejder fortæller dig at et udtryk ikke er tilladt i Habitica, selvom det er et udtryk, du ikke vidste var problematisk, er den beslutning endegyldig</strong>. Der vil blive slået særlig hårdt ned på skældsord, da de også går imod vores Vilkår og betingelser.",
|
||||
"commGuideList02F": "<strong>Undgå lange diskussioner af opsplittende emner i Værtshuset og hvor det ellers ikke ville være et passende emne</strong>. Hvis du føler nogen har sagt noget uhøfligt eller sårende, så svar dem ikke. Hvis nogen nævner noget der er tilladt ifølge retningslinjerne, men sårende for dig, er det okay høfligt at sige det til dem. Hvis det er imod retningslinjerne eller Vilkår og betingelser, så rapporter det og lad en moderator håndtere det. Hvis du er i tvivl, så rapporter beskeden.",
|
||||
"commGuideList02G": "<strong>Overhold omgående enhver anmodning fra en Moderator.</strong>. Dette kan inkludere, men er ikke begrænset til, at bede dig begrænse antallet af dine beskeder i et bestemt rum, at fjerne upassende indhold fra din profil, at bede dig om at tage din diskussion et mere passende sted osv.",
|
||||
"commGuideList02J": "<strong>Spam ikke</strong>. Spam inkluderer, men er ikke begrænset til: at skrive den samme kommentar eller spørgsmål flere steder, at lægge links op uden forklaring eller kontekst, at skrive meningsløse beskeder, at skrive adskillige promoverende beskeder om en Klan, Hold eller Udfordring, eller at skrive mange beskeder på én gang. At tigge om ædelsten eller et abonnement i ethvert chatrum eller via Privatbesked betragtes også som spam.<br/><br/>Det er op til moderatorerne at beslutte om noget er spam eller kan føre til spam, selv hvis du ikke mener du har spammet. For eksempel er det i orden at reklamere for en Klan en enkelt eller to gange, men adskillige beskeder samme da ville sikkert blive betragtet som spam, uanset hvor nyttig Klanen er!",
|
||||
"commGuidePara015": "Habitica har to slags sociale rum: offentlige og private. Offentlige rum inkluderer Værtshuset, offentlige klaner, GitHub, Trello og wiki'en. Private rum tæller private klaner, Holdchatten og private beskeder. Alle displaynavne og @brugernavne skal følge retningslinjerne for offentlige rum. Gå til Menu > Indstillinger > Profil for at ændre dit displaynavn eller @brugernavn i app'en. På Habiticas hjemmeside skal du gå til Bruger > Indstillinger.",
|
||||
"commGuidePara016": "Når du navigerer rundt i de offentlige rum af Habitica, er der nogle generelle regler for at sørge for, at alle er sikre og glade.",
|
||||
"commGuideList02A": "<strong>Respekter hinanden</strong>. Vær høflig, venlig og hjælpsom. Husk: Habiticanere kommer fra alle baggrunde og har haft vildt skiftende oplevelser og erfaringer. Det er en del af det, der gør Habitica så cool! At bygge et fællesskab betyder at vi respekterer og fejrer vores forskelle såvel som vores ligheder.",
|
||||
"commGuideList02B": "<strong>Adlyd alt i <a href='/static/terms' target='_blank'>Vilkår og betingelser</a></strong> i både offentlige og private rum.",
|
||||
"commGuideList02C": "<strong>Læg ikke billeder eller tekst op, der er voldeligt, truende eller seksuelt eksplicit/antydende, eller som promoverer diskrimination, fordomme, racisme, sexisme, had, chikane eller skade mod et individ eller en gruppe</strong>. Ikke engang som en joke. Dette inkluderer skældsord såvel som udtalelser. Ikke alle har den samme form for humor, så noget du anser som en joke er muligvis sårende for en anden.",
|
||||
"commGuideList02D": "<strong>Hold diskussioner passende for alle aldre</strong>. Dette betyder at 18+ emner ikke bør diskuteres i offentlige rum. Vi har mange unge Habiticanere der bruger hjemmesiden, og vores brugere kommer fra mange steder i verden. Vi vil gerne have, at Habitica er så rart og inkluderende et sted som muligt.",
|
||||
"commGuideList02E": "<strong>Undgå bandeord. Dette inkluderer mildere, religiøst baserede bandeord der kan være acceptable andre steder, og forkortede eller forklædte bandeord.</strong> Vi har folk fra alle religiøse og kulturelle baggrunde, og vi vil gerne sørge for at de alle føler sig trygge i offentlige rum. <strong>Hvis en moderator eller medarbejder fortæller dig at et udtryk ikke er tilladt i Habitica, selvom det er et udtryk, du ikke vidste var problematisk, er den beslutning endegyldig.</strong> Der vil blive slået særlig hårdt ned på skældsord, da de også går imod vores Vilkår og betingelser.",
|
||||
"commGuideList02F": "Undgå lange diskussioner af opsplittende emner i Værtshuset og hvor det ellers ikke ville være et passende emne. Hvis nogen siger noget der er tilladt under vores retningslinjer, men som er sårende for dig, er det okay høfligt at sige det til dem. Hvis nogen siger til dig, at du har gjort dem ubehageligt til mode, så tag et øjeblik til at tænke over dine ord frem for at svare i vrede. Hvis du føler, at en samtale er ved at blive overophedet, følelsesladet eller sårende, <strong>så hold op med at svare. Rapportér i stedet de relevante beskeder for at sende besked til os.</strong> Moderatorerne vil se på det, så hurtigt de kan. Du kan også sende en email til <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> og er velkommen til at sende screenshots, hvis det ville hjælpe os med at forstå sagen.",
|
||||
"commGuideList02G": "<strong>Overhold omgående enhver anmodning fra en Moderator.</strong> Dette kan inkludere, men er ikke begrænset til, at bede dig begrænse antallet af dine beskeder i et bestemt rum, at fjerne upassende indhold fra din profil, at bede dig om at tage din diskussion et mere passende sted osv. Begynd ikke at diskutere med moderatorerne. Hvis du har kommentarer til Habiticas moderering, så send en email til <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> for at komme i kontakt med vores community manager.",
|
||||
"commGuideList02J": "<strong>Spam ikke</strong>. Spam inkluderer, men er ikke begrænset til: at skrive den samme kommentar eller spørgsmål flere steder, <strong>at lægge links op uden forklaring eller kontekst</strong>, at skrive meningsløse beskeder, at skrive adskillige promoverende beskeder om en Klan, Hold eller Udfordring, eller at skrive mange beskeder på én gang. Hvis det at andre brugere følger et link vil give nogen form for afkast eller fordel til dig, skal det stå i beskeden, ellers vil det også blive regnet for spam. Det er op til moderatorerne at beslutte om noget er spam.",
|
||||
"commGuideList02K": "<strong>Undgå at skrive store overskrifter i offentlige chatrum, især Værtshuset</strong>. Ligesom ALL CAPS læses det som om du råber, og forstyrrer den behagelige stemning.",
|
||||
"commGuideList02L": "<strong>Vi fraråder kraftigt deling af personlig information -- især information der kan bruges til at identificere dig - i offentlige chatrum</strong>. Identificerende personlig information inkluderer, men er ikke begrænset til: din adresse, emailadresse og din API token/password. Det er for din sikkerheds skyld! Medarbejdere eller moderatorer kan fjerne sådanne beskeder efter forgodtbefindende. Hvis du bliver bedt om personlig information i en privat Klan, Hold eller Besked, anbefaler vi kraftigt at du høfligt afviser og gør medarbejderne og moderatorerne opmærksomme ved enten 1) at rapportere beskeden hvis det er i chatten tilhørende et Hold eller privat Klan, eller 2) at udfylde <a href='https://contact.habitica.com/' target='_blank'>Moderatorkontaktformularen</a> og vedlægger screenshots.",
|
||||
"commGuideList02L": "<strong>Vi fraråder kraftigt deling af personlig information -- især information der kan bruges til at identificere dig - i offentlige chatrum</strong>. Identificerende personlig information inkluderer, men er ikke begrænset til: din adresse, emailadresse og din API token/password. Det er for din sikkerheds skyld! Medarbejdere eller moderatorer kan fjerne sådanne beskeder efter forgodtbefindende. Hvis du bliver bedt om personlig information i en privat Klan, Hold eller Besked, anbefaler vi kraftigt at du høfligt afviser og gør medarbejderne og moderatorerne opmærksomme ved enten 1) at rapportere beskeden, eller 2) at sende en email til <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> og vedhæfte screenshots.",
|
||||
"commGuidePara019": "<strong>I private rum</strong> har brugere mere frihed til at diskutere hvilke emner de har lyst til, men de må stadig ikke gå imod Vilkår og betingelser, inklusiv at skrive nedværdigende skældsord eller noget diskriminerende, voldeligt eller truende indhold. Bemærk, at fordi navne på Udfordringer kan ses i vinderens offentlige profil skal ALLE navne på Udfrodringer adlyde retningslinjerne for offentlige rum, selv hvis de foregår i et privat rum.",
|
||||
"commGuidePara020": "<strong>Privatbeskeder</strong> har nogle ekstra retningslinjer. Hvis nogen har blokeret dig må du ikke kontakte dem på andre måder for at bede dem om at fjerne blokeringen. Derudover må du ikke sende privatbeskeder til andre for at bede om hjælp (fordi offentlige svar til spørgsmål om hjælp også kan hjælpe resten af fællesskabet). Sidst men ikke mindst må du ikke sende privatbeskeder til nogen for at tigge om ædelsten eller et abonnement, da dette kan anses som spamming.",
|
||||
"commGuidePara020A": "<strong>Hvis du ser et indlæg eller en privatbesked, du mener går imod retningslinjerne for offenlige rum, beskrevet ovenfor, eller hvis du ser indlæg eller privatbeskeder, der bekymrer dig eller gør dig utryg, kan du gøre Moderatorer og Medarbejdere opmærksomme på den ved at klikke på flag-ikonet for at rapportere det</strong>. En Medarbejder eller Moderator vil tage sig af situationen, så snart de kan. Bemærk venligst, at det at rapportere uskyldige indlæg går imod disse Retningslinjer (se \"Overtrædelser\" nedenunder). Du kan også kontakte Moderatorerne via formularen “<a href='https://contact.habitica.com/' target='_blank'>Kontakt Moderatorerne</a>.” Det kan være det bedste at gøre dette hvis der er flere problematiske indlæg af den samme person i forskellige Klaner, eller hvis situationen kræver en forklaring. Du kan kontakte os på dit eget sprog hvis det er lettere for dig. Vi bliver muligvis nødt til at bruge Google Translate, men vi vil gerne have at du føler dig tryg ved at tage kontakt til os, hvis du har et problem.",
|
||||
"commGuidePara020": "<strong>Privatbeskeder</strong> har nogle ekstra retningslinjer. Hvis nogen har blokeret dig må du ikke kontakte dem på andre måder for at bede dem om at fjerne blokeringen. Derudover må du ikke sende privatbeskeder til andre for at bede om hjælp (fordi offentlige svar til spørgsmål om hjælp også kan hjælpe resten af fællesskabet). Sidst men ikke mindst må du ikke sende privatbeskeder til nogen for at tigge om nogen form for service, der koster rigtige penge.",
|
||||
"commGuidePara020A": "<strong>Hvis du ser et indlæg eller en privatbesked, du mener går imod retningslinjerne for offentlige rum, beskrevet ovenfor, eller hvis du ser indlæg eller privatbeskeder, der bekymrer dig eller gør dig utryg, kan du gøre Moderatorer og Medarbejdere opmærksomme på den ved at klikke på flag-ikonet for at rapportere det</strong>. En Medarbejder eller Moderator vil tage sig af situationen, så snart de kan. Bemærk venligst, at det at rapportere uskyldige indlæg går imod disse retningslinjer (se \"Overtrædelser\" nedenunder). Du kan også kontakte Moderatorerne ved at emaile <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>. Det kan være det bedste at gøre dette, hvis der er flere problematiske indlæg af den samme person i forskellige Klaner, eller hvis situationen kræver en forklaring. Du kan kontakte os på dit eget sprog hvis det er lettere for dig. Vi bliver muligvis nødt til at bruge Google Translate, men vi vil gerne have at du føler dig tryg ved at tage kontakt til os, hvis du har et problem.",
|
||||
"commGuidePara021": "Herudover har nogen offentlige steder i Habitica ekstra retningslinjer.",
|
||||
"commGuideHeadingTavern": "Værtshuset",
|
||||
"commGuidePara022": "Værtshuset er det primære sted Habiticanere hænger ud. Kroværten Daniel holder stedet funklende rent, og Lemoness vil med glæde fremtrylle dig et glas lemonade mens du sidder og snakker. Bare husk…",
|
||||
@@ -31,8 +31,8 @@
|
||||
"commGuidePara029": "<strong>Offentlige Klaner er meget ligesom Værtshuset, bortset fra at de i stedet for at være til generelle samtaler har et bestemt tema</strong>. Chat i offentlige Klaner bør have fokus på dette tema. For eksempel ville medlemmer af Klanen Wordsmiths måske blive fornærmede, hvis samtalen pludselig handler om havearbejde, og Klanen for Dragefans har nok ikke meget interesse i at tyde oldtidens runer. Nogle Klaner er meget afslappede om dette end andre, men forsøg generelt <strong>at holde dig til emnet</strong>!",
|
||||
"commGuidePara031": "I nogle offentlige Klaner omtales der følsomme emner som depression, religion, politik osv. Dette er helt i orden, så længe samtalerne i Klanen ikke bryder nogle Vilkår og betingelser eller Regler for offentlige rum, og så længe de holder sig til Klanens emne.",
|
||||
"commGuidePara033": "<strong>Offentlige Klaner må IKKE indeholde indhold for aldersgruppen 18+. Hvis de planlægger jævnligt at diskutere følsomt indhold, skal dette stå i Klanens beskrivelse</strong>. Dette er for at holde Habitica sikkert of trygt for alle.",
|
||||
"commGuidePara035": "<strong>Hvis en Klan omhandler flere forskellige følsomme emner, er det hensynsfuldt over for dine med-Habiticanere at skrive din kommentar efter en advarsel (fx \"Advarsel: Omtaler selvskade\")</strong>. Disse kan karakteriseres som trigger warnings og/eller noter om indholdet, og Klaner kan have deres egne regler om disse udover dem, der er givet her. Hvis det er muligt, så brug venligst <a href='http://habitica.fandom.com/wiki/Markdown_Cheat_Sheet' target='_blank'>markdown</a> for at gemme det potentielt følsomme indhold under flere linjeskift, så de, der ikke ønsker at læse det, kan scrolle forbi uden at se indholdet. Habiticas medarbejdere og moderatorer kan stadig fjerne dette indhold efter eget skøn.",
|
||||
"commGuidePara036": "Desuden bør følsomt indhold stadig være aktuelt for Klanens emne - at tale om selvskade i en Klan hvor fokus er på at kæmpe mod depression giver mening, men er mindre passende i en Klan for musik. Hvis du ser nogen, som gentagne gange går imod denne retningslinje, især efter at være blevet bedt om at stoppe flere gange, så rapportér venligst deres indlæg og gør moderatorerne opmærksom på dette via <a href='https://contact.habitica.com/' target='_blank'>Moderatorkontaktformularen</a>.",
|
||||
"commGuidePara035": "<strong>Hvis en Klan omhandler flere forskellige følsomme emner, er det hensynsfuldt over for dine med-Habiticanere at skrive din kommentar efter en advarsel (fx \"Advarsel: Omtaler selvskade\")</strong>. Disse kan karakteriseres som trigger warnings og/eller noter om indholdet, og Klaner kan have deres egne regler om disse udover dem, der er givet her. Hvis det er muligt, så brug venligst <a href='https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet' target='_blank'>markdown</a> for at gemme det potentielt følsomme indhold under flere linjeskift, så de, der ikke ønsker at læse det, kan scrolle forbi uden at se indholdet. Habiticas medarbejdere og moderatorer kan stadig fjerne dette indhold efter eget skøn.",
|
||||
"commGuidePara036": "Desuden bør følsomt indhold stadig være aktuelt for Klanens emne - at tale om selvskade i en Klan hvor fokus er på at kæmpe mod depression giver mening, men er mindre passende i en Klan for musik. Hvis du ser nogen, som gentagne gange går imod denne retningslinje, især efter at være blevet bedt om at stoppe flere gange, så rapportér deres beskeder.",
|
||||
"commGuidePara037": "<strong>Ingen Klaner, hverken Offentlige eller Private, bør oprettes med det formål at angribe en gruppe eller et individ</strong>. At oprette en sådan Klan er grundlæg for øjeblikkelig bortvisning fra Habitica. Nedkæmp dårlige vaner, ikke dine med-eventyrere!",
|
||||
"commGuidePara038": "<strong>Alle Udfordringer i Værtshuset og Offentlige Klaner skal også følge disse regler</strong>.",
|
||||
"commGuideHeadingInfractionsEtc": "Overtrædelser, Konsekvenser og Genskabelse",
|
||||
@@ -44,24 +44,24 @@
|
||||
"commGuidePara053": "De følgende er eksempler på større overtrædelser. Listen er ikke fyldestgørende.",
|
||||
"commGuideList05A": "Brud på Betingelser og Vilkår",
|
||||
"commGuideList05B": "Hadtale/billeder, Chikane/stalking, Cyber-mobning, Flaming og Trolling",
|
||||
"commGuideList05C": "Brud på Prøvetid",
|
||||
"commGuideList05D": "At udgive sig for at være Ansat eller Moderator",
|
||||
"commGuideList05E": "Gentagne Moderate Overtrædelser",
|
||||
"commGuideList05C": "Brud på prøvetid",
|
||||
"commGuideList05D": "At udgive sig for at være Ansat eller Moderator - dette inkluderer at påstå, at brugerskabte platforme, der ikke at associerede med Habitica, er officielle og/eller modereret af Habitica eller deres mods/medarbejdere",
|
||||
"commGuideList05E": "Gentagne moderate overtrædelser",
|
||||
"commGuideList05F": "Oprettelse af en ekstra konto for at undgå konsekvenser (for eksempel at oprette en ny konto til at chatte med, efter at have fået frataget ens chat-privilegier)",
|
||||
"commGuideList05G": "Bevidst bedrag af Ansatte eller Moderatorer med det formål at undgå konsekvenser eller bringe en anden bruger i vanskeligheder",
|
||||
"commGuideHeadingModerateInfractions": "Moderate Overtrædelser",
|
||||
"commGuideHeadingModerateInfractions": "Moderate overtrædelser",
|
||||
"commGuidePara054": "Moderate overtrædelser gør ikke fællesskabet usikkert, men de gør det ubehageligt. Disse overtrædelser har moderate konsekvenser. Når de står sammen med andre overtrædelser, kan konsekvenserne blive større.",
|
||||
"commGuidePara055": "De følgende er eksempler på Moderate Overtrædelser. Listen er ikke endelig.",
|
||||
"commGuidePara055": "De følgende er eksempler på moderate overtrædelser. Listen er ikke fyldestgørende.",
|
||||
"commGuideList06A": "At ignorere, opføre respektløst overfor, eller skændes med en Moderator. Dette inkluderer offentlige at klage over moderatorer eller andre brugere, offentligt at forherlige eller forsvare bortviste brugere, eller at debatterer hvorvidt en moderators handlinger var passende. Hvis du er bekymret om en af reglerne eller en Moderators opførsel, så kontakt venligst de ansatte via email (<a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>).",
|
||||
"commGuideList06B": "Backseat Modding. For at tydeliggøre en relevant pointe: En venlig påmindelse om reglerne er helt fint. Backseat modding består i at fortælle, kræve, og/eller stærkt antyde at nogen bør gøre som du siger for at rette op på en fejl. Du kan gøre nogen opmærksom på at de har begået en overtrædelse, men forlang venligst ikke at de skal gøre noget - for eksempel er det bedre at sige \"Bare så du ved det er det ikke så godt at bande i Værtshuset, så det kan være du skulle slette det\" end \"Jeg er nødt til at bede dig slette det indlæg.\"",
|
||||
"commGuideList06C": "At rapportere uskyldige indlæg med vilje.",
|
||||
"commGuideList06D": "Gentagne gange at bryde Retningslinjerne for offentlige rum",
|
||||
"commGuideList06E": "Gentagne gange at begå mindre overtrædelser",
|
||||
"commGuideHeadingMinorInfractions": "Mindre Overtrædelser",
|
||||
"commGuideHeadingMinorInfractions": "Mindre overtrædelser",
|
||||
"commGuidePara056": "Mindre overtrædelser har kun mindre konsekvenser, men er stadig ikke anbefalede. Hvis de sker gentagne gange kan det føre til større konsekvenser.",
|
||||
"commGuidePara057": "De følgende er eksempler på Mindre Overtrædelser. Listen er ikke fyldestgørende.",
|
||||
"commGuidePara057": "De følgende er eksempler på mindre overtrædelser. Listen er ikke fyldestgørende.",
|
||||
"commGuideList07A": "Førstegangsbrud på Retningslinjer for Offentlige Steder",
|
||||
"commGuideList07B": "Alle udsagn eller handlinger, som udløser et \"Lad venligst være\". Når en Moderator er nødt til at sige \"Lad venligst være med det\" til en bruger, kan det tælle som en meget lille overtrædelse for den bruger. Et eksempel kunne være \"Lad venligst være med at blive ved med at argumentere for indførslen af den funktion, når vi har sgat til dig adskillige gange at det ikke kan lade sig gøre.\" I mange tilfælde vil 'Lad venligst være' blot være konsekvensen af overtrædelsen, men hvis Moderatorerne er nødt til at bede den samme bruger flere gange om at holde op, vil de Mindre Overtrædelser begynde at tælle som Moderate Overtrædelser.",
|
||||
"commGuideList07B": "Alle udsagn eller handlinger, som udløser et \"lad venligst være\" fra en moderator. Når du offentligt bliver bedt om at stoppe en handling, kan dette i sig selv tælle som en mindre overtrædelse. Hvis mods er nødt til gentagne gange at irettesætte den samme person, kan det komme til at tælle som en grovere overtrædelse",
|
||||
"commGuidePara057A": "Nogle indlæg kan blive skjult fordi de indeholder følsom information eller kan give folk det forkerte indtryk. Typisk gælder dette ikke som en overtrædelser, især ikke den første gang det sker!",
|
||||
"commGuideHeadingConsequences": "Konsekvenser",
|
||||
"commGuidePara058": "I Habitica - som i virkeligheden - har alle handlinger også konsekvenser, hvad enten det er at komme i bedre form fordi du har løbet, få huller i tænderne fordi du har spist for meget sukker, eller bestå et fag fordi du har studeret.",
|
||||
@@ -71,53 +71,63 @@
|
||||
"commGuideList08B": "hvad konsekvensen er",
|
||||
"commGuideList08C": "hvad der skal til for at rette fejlen og genoprette din status, hvis muligt.",
|
||||
"commGuidePara060A": "Hvis situationen påkræver det kan du også modtage en PM eller email plus et indlæg i det forum, hvor overtrædelsen blev begået. I nogle tilfælde bliver du slet ikke irettesat offentligt.",
|
||||
"commGuidePara060B": "Hvis din konto bortvises (en alvorlig konsekvens), vil du ikke være i stand til at logge på Habitica, og vil få en fejl-besked når du prøver. <strong>Hvis du ønsker at undskylde eller argumentere for genoprettelse af din konto, så skriv venligst en email til de ansatte på <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> med dit UUID (unikke bruger-ID)</strong> (som vil blive oplyst i fejl-beskeden). Det er <strong>dit</strong> ansvar at tage kontakt, hvis du ønsker en genovervejelse af situationen, eller genoprettelse.",
|
||||
"commGuideHeadingSevereConsequences": "Eksempler på Større Konsekvenser",
|
||||
"commGuidePara060B": "Hvis din konto bortvises (en alvorlig konsekvens), vil du ikke være i stand til at logge på Habitica, og vil få en fejl-besked når du prøver. <strong>Hvis du ønsker at undskylde eller argumentere for genoprettelse af din konto, så skriv venligst en email til de ansatte på <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> med dit bruger-ID</strong> (som vil blive oplyst i fejlbeskeden) eller @brugernavn. Det er <strong>dit</strong> ansvar at tage kontakt, hvis du ønsker en genovervejelse af situationen, eller genoprettelse.",
|
||||
"commGuideHeadingSevereConsequences": "Eksempler på større konsekvenser",
|
||||
"commGuideList09A": "Kontoudelukkelser (se ovenstående)",
|
||||
"commGuideList09C": "Permanent deaktivering (\"indefrysning\") af fremskridt i Bidragsyder-niveauer",
|
||||
"commGuideHeadingModerateConsequences": "Eksempler på Moderate Konsekvenser",
|
||||
"commGuideHeadingModerateConsequences": "Eksempler på moderate konsekvenser",
|
||||
"commGuideList10A": "Begrænsede offentlige og/eller private chatprivilegier",
|
||||
"commGuideList10A1": "Hvis dine handlinger resulterer i fradragelse af dine rettigheder til chatten, vil en Moderator eller Ansat sende dig en privatbesked og/eller et indlæg i det forum du blev suspenderet fra for at fortælle dig grunden til og længden af din suspendering. Efter denne periode vil du få dine chatrettigheder tilbage, forudsat at du er villig til at ændre ved den opførsel du blev suspenderet for og overholde Fællesskabets Retningslinjer.",
|
||||
"commGuideList10A1": "Hvis dine handlinger resulterer i fradragelse af dine rettigheder til chatten, vil en Moderator eller Ansat sende dig en privatbesked og/eller et indlæg i det forum du blev suspenderet fra for at fortælle dig grunden til og længden af din suspendering, og/eller hvad du skal gøre for at få dine chatprivilegier tilbage. Du vil få dem igen, hvis du høfligt følger de anvisninger du får, og erklærer dig villig til at følge vores Retningslinjer for Fællesskabet og Betingelser & Vilkår",
|
||||
"commGuideList10C": "Begrænsede Klan/Udfordringsoprettelsesprivilegier",
|
||||
"commGuideList10D": "Midlertidig deaktivering (\"indefrysning\") af fremskridt i Bidragsyder-niveauer",
|
||||
"commGuideList10E": "Degradering af Bidragsyder-niveauer",
|
||||
"commGuideList10F": "Sætte brugere på \"Prøvetid\"",
|
||||
"commGuideHeadingMinorConsequences": "Eksempler på Mindre Konsekvenser",
|
||||
"commGuideList10F": "At sætte brugere på \"prøvetid\"",
|
||||
"commGuideHeadingMinorConsequences": "Eksempler på mindre konsekvenser",
|
||||
"commGuideList11A": "Påmindelser om Retningslinjer for Offentlige Steder",
|
||||
"commGuideList11B": "Advarsler",
|
||||
"commGuideList11C": "Anmodninger",
|
||||
"commGuideList11D": "Slettelser (Moderatorer/Ansatte kan slette problematisk indhold)",
|
||||
"commGuideList11E": "Rettelser (Moderatorer/Ansatte kan slette problematisk indhold)",
|
||||
"commGuideList11E": "Rettelser (Moderatorer/Ansatte kan redigere problematisk indhold)",
|
||||
"commGuideHeadingRestoration": "Genoprettelse",
|
||||
"commGuidePara061": "Habitica er en land dedikeret til forbedring, og vi tror på at give en chance til <strong>Hvis du begår en overtrædelse og bliver udsat for en konsekvens som følge, så se det som en mulighed for at evaluere dine handlinger og forbedre din opførsel som medlem af fællesskabet</strong>.",
|
||||
"commGuidePara062": "Den bekendtgørelse, besked og/eller email du modtager, der forklarer konsekvenserne af dine handlinger, er en god kilde til information. Hold dig til de begrænsninger der er blevet påført, og bestræb dig på at møde de krav, der kan få eventuelle straffe ophævet.",
|
||||
"commGuidePara063": "Hvis du ikke forstår konsekvenserne eller arten af din overtrædelse, så bed Medarbejderne/Moderatorerne om hjælp, så du kan undgå at begå overtrædelser i fremtiden. Hvis du føler at en bestemt beslutning var uretfærdig, kan du kontakte de ansatte for at diskutere det på <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>.",
|
||||
"commGuideHeadingMeet": "Mød de Ansatte og Moderatorerne!",
|
||||
"commGuidePara006": "Habitica har nogle utrættelige omvandrende riddere, der kæmper sammen med de ansatta for at holde fællesskabet roligt, tilfreds, og frit for trolde. Hver har et specifikt domæne, men vil sommetider blive kaldt ind for at hjælpe i andre sociale sfærer.",
|
||||
"commGuidePara006": "Habitica har nogle utrættelige omvandrende riddere, der kæmper sammen med de ansatte for at holde fællesskabet roligt, tilfreds, og frit for trolde. Hver har et specifikt domæne, men vil sommetider blive kaldt ind for at hjælpe i andre sociale sfærer.",
|
||||
"commGuidePara007": "Ansatte har lilla tags markeret med kroner. Deres titel er \"Heltemodig\".",
|
||||
"commGuidePara008": "Moderatorer har mørkeblå tags markeret med stjerner. Deres titel er \"Beskytter\". Den eneste undtagelse er Bailey, der som NPC har et sort og grønt tag markeret med en stjerne.",
|
||||
"commGuidePara009": "De nuværende Ansatte er (fra venstre mod højre):",
|
||||
"commGuideAKA": "<%= habitName %> aka <%= realName %>",
|
||||
"commGuidePara008": "Moderatorer har mørkeblå tags markeret med stjerner. Deres titel er \"Beskytter\".",
|
||||
"commGuidePara009": "De nuværende ansatte er (fra venstre mod højre):",
|
||||
"commGuideAKA": "<%= habitName %>, aka <%= realName %>",
|
||||
"commGuideOnTrello": "<%= trelloName %> på Trello",
|
||||
"commGuideOnGitHub": "<%= gitHubName %> på GitHub",
|
||||
"commGuidePara010": "Der er også flere Moderatorer, der hjælper de ansatte. Disse er omhyggeligt udvalgt, så vis dem respekt og lyt til deres forslag.",
|
||||
"commGuidePara011": "De nuværende Moderatorer er (fra venstre mod højre):",
|
||||
"commGuidePara011b": "På GitHub/Wikia",
|
||||
"commGuidePara011c": "på Wikia",
|
||||
"commGuidePara011": "De nuværende moderatorer er (fra venstre mod højre):",
|
||||
"commGuidePara011b": "på GitHub/Fandom",
|
||||
"commGuidePara011c": "på wiki'en",
|
||||
"commGuidePara011d": "på GitHub",
|
||||
"commGuidePara012": "Hvis du har et problem eller bekymring, der drejer sig om en bestemt Moderator, så send venligst en email til vores Medarbejdere (<a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>).",
|
||||
"commGuidePara013": "Brugere kommer og går i et fællesskab så stort som Habitica. Sommetider bliver en medarbejder eller moderator nødt til at fralægge sig deres ædle kappe, og slappe lidt af. De følgende er Ansatte og Moderatorer Emeritus. De løfter ikke længere et ansvar som Ansat eller Moderator, men vi vil stadig gerne mindes deres indsats!",
|
||||
"commGuidePara014": "Ansatte og Moderatorer Emeritus:",
|
||||
"commGuideHeadingFinal": "Den Sidste Sektion",
|
||||
"commGuidePara067": "Det var så det, modige Habiticaner - Retningslinjerne for fællesskabet! Tør sveden af panden og giv dig selv nogle Erfaringpoint for at læse det hele. Hvis du har nogle spørgsmål om disse Retningslinjer for fællesskabet, så tag venligst fat i os via <a href='https://contact.habitica.com/' target='_blank'>Moderatorkontaktformularen</a>, og vi vil med glæde forsøge at gøre tingene klart for dig.",
|
||||
"commGuidePara068": "Tag afsted, modige eventyrer, og bekæmp nogle Daglige!",
|
||||
"commGuidePara014": "Ansatte og Moderatorer emeritus:",
|
||||
"commGuideHeadingFinal": "Den sidste sektion",
|
||||
"commGuidePara067": "Det var så det, modige Habiticaner - Retningslinjerne for fællesskabet! Tør sveden af panden og giv dig selv nogle Erfaringpoint for at læse det hele. Hvis du har nogle spørgsmål om disse Retningslinjer for fællesskabet, så tag venligst fat i os via <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>, og vi vil med glæde forsøge at gøre tingene klart for dig.",
|
||||
"commGuidePara068": "Tag afsted, modige eventyrer, og nedkæmp nogle Daglige!",
|
||||
"commGuideHeadingLinks": "Nyttige links",
|
||||
"commGuideLink01": "<a href='/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a' target='_blank'>Habitica Help: Ask a Question</a>: en Klan hvor brugere kan stille spørgsmål!",
|
||||
"commGuideLink02": "<a href='http://habitica.fandom.com/wiki/Habitica_Wiki' target='_blank'>Wiki'en</a>: den største samling af information om Habitica.",
|
||||
"commGuideLink03": "<a href='https://github.com/HabitRPG/habitica' target='_blank'>GitHub</a>: til bug-rapporter eller hjælp med kodning!",
|
||||
"commGuideLink02": "<a href='https://habitica.fandom.com/wiki/Habitica_Wiki' target='_blank'>Wiki'en</a>: den største samling af information om Habitica.",
|
||||
"commGuideLink03": "<a href='https://github.com/HabitRPG/habitica' target='_blank'>GitHub</a>: for at hjælpe med kodning!",
|
||||
"commGuideLink04": "<a href='https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link' target='_blank'>Feedbackformularen</a>: til forslag til hjemmesiden og app'en.",
|
||||
"commGuideLink05": "<a href='https://trello.com/b/mXK3Eavg/' target='_blank'>Den mobile Trello</a>: til at bede om funktioner til vores apps.",
|
||||
"commGuideLink06": "<a href='https://trello.com/b/vwuE9fbO/' target='_blank'>Kunst-Trello</a>: til at indsende pixel art.",
|
||||
"commGuideLink07": "<a href='https://trello.com/b/nnv4QIRX/' target='_blank'>Quest-Trello</a>: til at indsende tekst til quests.",
|
||||
"commGuidePara069": "Følgende talentfulde kunstnere har bidraget med disse illustrationer:"
|
||||
"commGuidePara069": "Følgende talentfulde kunstnere har bidraget med disse illustrationer:",
|
||||
"commGuidePara017": "Her er den hurtige version, men vi opfordrer dig til at læse den mere detaljerede udgave nedenunder:",
|
||||
"commGuideList01A": "Vores Betingelser & Vilkår gælder alle steder i Habitica, inklusiv private klaner, holdchatten og private beskeder.",
|
||||
"commGuideList02M": "Bed eller tig ikke andre om at give dig ædelsten, et abonnement eller medlemskab i gruppeplaner. Dette er ikke tilladt at gøre i hverken Værtshuset, offentlige eller private rum, eller i privatbeskeder. Hvis du får en besked med en anmodning om ting, der koster rigtige penge, så rapportér dem venligst. Gentaget eller groft tiggeri, især efter en advarsel er blevet udstedt, kan resulterer i en blokering af din konto.",
|
||||
"commGuideList01B": "Forbudt: Al kommunikation der er voldeligt, truende, opfordrer til diskrimination osv., inklusiv memes, billeder og jokes.",
|
||||
"commGuideList01C": "Al samtale skal være passende for enhver aldersgruppe og ikke indeholde skælds- ellers bandeord.",
|
||||
"commGuideList01D": "Følg venligst moderatorernes anvisninger.",
|
||||
"commGuideList01E": "Start og deltag ikke i diskussioner i Værtshuset, der kan føre til skænderier.",
|
||||
"commGuideList01F": "Bed ikke andre om at give dig ting der koster rigtige penge, spam ikke, og skriv ikke beskeder i all caps eller stor overskriftstekst.",
|
||||
"commGuideList05H": "Grove eller gentagne forsøg på at bedrage eller presse andre spillere til at give dig genstande/services, der koster rigtige penge",
|
||||
"commGuideList09D": "Fjernelse af Bidragsyder-niveauer"
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
"questEggHedgehogText": "Pindsvin",
|
||||
"questEggHedgehogMountText": "Pindsvin",
|
||||
"questEggHedgehogAdjective": "et stikkende",
|
||||
"questEggDeerText": "Rådyr",
|
||||
"questEggDeerMountText": "Rådyr",
|
||||
"questEggDeerText": "Hjort",
|
||||
"questEggDeerMountText": "Hjort",
|
||||
"questEggDeerAdjective": "et elegant",
|
||||
"questEggEggText": "Æg",
|
||||
"questEggEggMountText": "Æggekurv",
|
||||
@@ -200,7 +200,7 @@
|
||||
"hatchingPotionEmber": "Glødende",
|
||||
"hatchingPotionThunderstorm": "Tordenvejr",
|
||||
"hatchingPotionGhost": "Spøgelse",
|
||||
"hatchingPotionRoyalPurple": "Royal lilla",
|
||||
"hatchingPotionRoyalPurple": "Purpur",
|
||||
"hatchingPotionHolly": "Kristtorn",
|
||||
"hatchingPotionCupid": "Amor",
|
||||
"hatchingPotionShimmer": "Glimmer",
|
||||
@@ -365,5 +365,12 @@
|
||||
"questEggDolphinAdjective": "en munter",
|
||||
"questEggDolphinMountText": "Delfin",
|
||||
"questEggDolphinText": "Delfin",
|
||||
"hatchingPotionDessert": "Konfekt"
|
||||
"hatchingPotionDessert": "Konfekt",
|
||||
"hatchingPotionFluorite": "Fluorid",
|
||||
"hatchingPotionSunset": "Solnedgangs",
|
||||
"hatchingPotionMoonglow": "Månelys",
|
||||
"hatchingPotionSolarSystem": "Solsystems",
|
||||
"hatchingPotionOnyx": "Onyks",
|
||||
"hatchingPotionPorcelain": "Porcelæns",
|
||||
"hatchingPotionVirtualPet": "Virtuelt kæledyrs"
|
||||
}
|
||||
|
||||
@@ -49,9 +49,10 @@
|
||||
"balance": "Balance",
|
||||
"playerTiers": "Spillertrin",
|
||||
"tier": "Trin",
|
||||
"conRewardsURL": "http://habitica.fandom.com/wiki/Contributor_Rewards",
|
||||
"conRewardsURL": "https://habitica.fandom.com/wiki/Contributor_Rewards",
|
||||
"surveysSingle": "Hjalp Habitica med at vokse ved at udfylde et spørgeskema eller var en stor hjælp ved test. Tusind tak!",
|
||||
"surveysMultiple": "Hjalp Habitica med at vokse ved <%= count %> lejligheder, enten ved at udfylde et spørgeskema eller hjalp med et større test-arbejde. Tusind tak!",
|
||||
"blurbHallPatrons": "Dette er Protektorernes Sal, hvor vi ærer de ædle eventyrere, der støttede Habiticas originale Kickstarter. Vi takker dem for at hjælpe os med at vække Habitica til live!",
|
||||
"blurbHallContributors": "Dette er Bidragsydernes Sal, hvor dem, der har bidraget med open-source materiale til Habitica, bliver æret. Om det er gennem kode, grafik, musik, tekst eller bare generel hjælpsomhed, har de modtaget <a href='http://habitica.fandom.com/wiki/Contributor_Rewards' target='_blank'> ædelsten, eksklusivt udstyr</a>, og <a href='http://habitica.fandom.com/wiki/Contributor_Titles' target='_blank'>prestigefyldte titler</a>. Du kan også bidrage til Habitica! <a href='http://habitica.fandom.com/wiki/Contributing_to_Habitica' target='_blank'> Find ud af mere her. </a>"
|
||||
"blurbHallContributors": "Dette er Bidragsydernes Sal, hvor dem, der har bidraget med open-source materiale til Habitica, bliver æret. Om det er gennem kode, grafik, musik, tekst eller bare generel hjælpsomhed, har de modtaget <a href='https://habitica.fandom.com/wiki/Contributor_Rewards' target='_blank'> ædelsten, eksklusivt udstyr</a>, og <a href='https://habitica.fandom.com/wiki/Contributor_Titles' target='_blank'>prestigefyldte titler</a>. Du kan også bidrage til Habitica! <a href='https://habitica.fandom.com/wiki/Contributing_to_Habitica' target='_blank'> Find ud af mere her. </a>",
|
||||
"noPrivAccess": "Du har ikke de påkrævede rettigheder."
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"dontDespair": "Bare rolig!",
|
||||
"deathPenaltyDetails": "Du mistede et Niveau, dit Guld og et stykke Udstyr, men du kan få det hele igen med hårdt arbejde! Held og lykke - du skal nok klare den.",
|
||||
"refillHealthTryAgain": "Genfyld Liv og Prøv Igen",
|
||||
"dyingOftenTips": "Sker dette ofte? <a href='http://habitica.fandom.com/wiki/Death_Mechanics#Strategies_for_Staying_Alive' target='_blank'>Her er nogle fif!</a>",
|
||||
"dyingOftenTips": "Sker dette ofte? <a href='https://habitica.fandom.com/wiki/Death_Mechanics#Strategies_for_Staying_Alive' target='_blank'>Her er nogle fif!</a>",
|
||||
"losingHealthWarning": "Pas på - du mister Liv!",
|
||||
"losingHealthWarning2": "Lad ikke dit Liv falde til nul! Hvis du gør det vil du tabe et niveau, alt dit guld og et stykke udstyr.",
|
||||
"toRegainHealth": "For at genopfylde Liv:",
|
||||
@@ -14,4 +14,4 @@
|
||||
"lowHealthTips4": "Hvis en Daglig ikke er forfalden på en given dag, kan du deaktivere den ved at klikke på blyanten.",
|
||||
"goodLuck": "Held og lykke!",
|
||||
"cannotRevive": "Du kan ikke genoplive hvis du ikke er død"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"defaultHabit2Text": "Spis junk food (Klik på blyanten for at redigere)",
|
||||
"defaultHabit3Text": "Tag trapperne/elevatoren (Klik på blyanten for at redigere)",
|
||||
"defaultHabit4Text": "Tilføj en opgave til Habitica",
|
||||
"defaultHabit4Notes": "Enten en Vane, en Daglig eller en To-Do",
|
||||
"defaultHabit4Notes": "Enten en Vane, en Daglig eller en To Do",
|
||||
"defaultTodo1Text": "Begynd at bruge Habitica (Markér mig som færdig!)",
|
||||
"defaultTodoNotes": "Du kan enten færdiggøre denne To-Do, ændre den, eller fjerne den.",
|
||||
"defaultTodoNotes": "Du kan enten færdiggøre denne To Do, ændre den eller fjerne den.",
|
||||
"defaultReward1Text": "15 minutters pause",
|
||||
"defaultReward2Text": "Beløn dig selv",
|
||||
"defaultReward2Notes": "Se fjernsyn, spil et spil, spis noget guf, det er op til dig!",
|
||||
|
||||