Merge branch 'develop' into sabrecat/panel-subscription
@@ -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,11 +1,11 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.239.0",
|
||||
"version": "4.242.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/register": "^7.18.9",
|
||||
"@google-cloud/trace-agent": "^5.1.6",
|
||||
"@parse/node-apn": "^5.1.3",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -15880,9 +15880,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",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"chai": "^4.3.6",
|
||||
"core-js": "^3.23.5",
|
||||
"core-js": "^3.24.1",
|
||||
"dompurify": "^2.3.10",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
|
||||
@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
execSync('npm run storybook:build', {
|
||||
/* execSync('npm run storybook:build', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}); */
|
||||
}
|
||||
|
||||
@@ -27015,6 +27015,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 +36718,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 +37138,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 +37658,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 +38078,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 +38348,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 +39008,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 +39498,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 +40723,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 +41303,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 +41828,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 +42248,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 +42768,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 +43188,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 +43458,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 +44118,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 +44608,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 +45833,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 +46413,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 +46943,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 +47363,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 +47883,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 +48303,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 +48573,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 +49238,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 +49728,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 +50953,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 +51533,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 +52068,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 +52503,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 +53043,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 +53478,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 +53763,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 +54443,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 +54948,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 +56198,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 +56798,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 +57133,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -757,8 +757,7 @@ export default {
|
||||
}, 500),
|
||||
sanitizeRedirect (redirect) {
|
||||
if (!redirect) return '/';
|
||||
let sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
sanitizedString = `/${sanitizedString}`;
|
||||
const sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
return sanitizedString;
|
||||
},
|
||||
async register () {
|
||||
|
||||
@@ -112,10 +112,8 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'group._id': {
|
||||
async groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
'group._id': function groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="group-plans-update"
|
||||
title="New Shared Task Board"
|
||||
size="lg"
|
||||
hide-footer="hide-footer"
|
||||
:no-close-on-backdrop="true"
|
||||
:no-close-on-esc="true"
|
||||
:centered="true"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
class="w-100 d-flex pt-2 justify-content-center"
|
||||
>
|
||||
<h2
|
||||
class="mx-auto mt-4"
|
||||
v-once
|
||||
>
|
||||
{{ $t('newGroupsWelcome') }}
|
||||
</h2>
|
||||
<div
|
||||
class="svg-icon color close-x ml-auto my-auto"
|
||||
aria-hidden="true"
|
||||
tabindex="0"
|
||||
@click="close()"
|
||||
@keypress.enter="close()"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
<img
|
||||
class="task-columns"
|
||||
src="~@/assets/images/group-plans/task-columns.png"
|
||||
srcset="~@/assets/images/group-plans/task-columns@2x.png 2x,
|
||||
~@/assets/images/group-plans/task-columns@3x.png 3x"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex flex-column justify-content-center p-4"
|
||||
>
|
||||
<div class="d-flex align-items-center justify-content-center mb-3">
|
||||
<div
|
||||
class="svg-icon sparkles my-auto mr-3"
|
||||
v-html="icons.sparkles"
|
||||
>
|
||||
</div>
|
||||
<h3
|
||||
class="my-auto"
|
||||
v-once
|
||||
>
|
||||
{{ $t('newGroupsWhatsNew') }}
|
||||
</h3>
|
||||
<div
|
||||
class="svg-icon sparkles my-auto ml-3 flip-x"
|
||||
v-html="icons.sparkles"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="mb-4 px-4"
|
||||
>
|
||||
<li>{{ $t('newGroupsBullet01') }}</li>
|
||||
<li>{{ $t('newGroupsBullet02') }}</li>
|
||||
<li>{{ $t('newGroupsBullet03') }}</li>
|
||||
<li>{{ $t('newGroupsBullet04') }}</li>
|
||||
<li>{{ $t('newGroupsBullet05') }}</li>
|
||||
<li>{{ $t('newGroupsBullet06') }}</li>
|
||||
<li>{{ $t('newGroupsBullet07') }}</li>
|
||||
<li>{{ $t('newGroupsBullet08') }}</li>
|
||||
<li>{{ $t('newGroupsBullet09') }}</li>
|
||||
<li>{{ $t('newGroupsBullet10') }}
|
||||
<ul class="p-0">
|
||||
<li v-html="$t('newGroupsBullet10a')"></li>
|
||||
<li v-html="$t('newGroupsBullet10b')"></li>
|
||||
<li v-html="$t('newGroupsBullet10c')"></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="mx-auto"
|
||||
v-html="$t('newGroupsVisitFAQ')"
|
||||
></div>
|
||||
<div
|
||||
class="mx-auto"
|
||||
>
|
||||
{{ $t('newGroupsEnjoy') }}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary mt-4 mb-1 mx-auto"
|
||||
@click="close()"
|
||||
@keypress.enter="close()"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
#group-plans-update {
|
||||
.modal-content {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
max-width: 566px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-image: linear-gradient(64deg, $purple-200, $purple-300 55%);
|
||||
height: 136px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-header, .modal-body, .modal-footer {
|
||||
padding: 0px;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
h2 {
|
||||
color: $white;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
line-height: 24px;
|
||||
list-style-type: square;
|
||||
li ul {
|
||||
list-style-type: none;
|
||||
li::before {
|
||||
margin-right: 1rem;
|
||||
font-size: 16px;
|
||||
content: '▫';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.close-x {
|
||||
color: $purple-400;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
right: 18px;
|
||||
top: 18px;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
width: 32px;
|
||||
height: 17px;
|
||||
|
||||
&.flip-x {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.task-columns {
|
||||
position: absolute;
|
||||
top: 84px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
sparkles,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$store.dispatch('user:set', { 'flags.tour.groupPlans': -2 });
|
||||
this.$root.$emit('bv::hide::modal', 'group-plans-update');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -28,7 +28,7 @@
|
||||
{{ $t('groupBilling') }}
|
||||
</router-link>
|
||||
</secondary-menu>
|
||||
<div class="col-12">
|
||||
<div class="col-12 px-0">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="standard-page">
|
||||
<div
|
||||
class="standard-page"
|
||||
@click="openCreateBtn ? openCreateBtn = false : null"
|
||||
>
|
||||
<group-plan-overview-modal />
|
||||
<task-modal
|
||||
ref="taskModal"
|
||||
@@ -7,65 +10,80 @@
|
||||
:purpose="taskFormPurpose"
|
||||
:group-id="groupId"
|
||||
@cancel="cancelTaskModal()"
|
||||
@taskCreated="taskCreated"
|
||||
@taskEdited="taskEdited"
|
||||
@taskCreated="loadTasks"
|
||||
@taskEdited="loadTasks"
|
||||
@taskDestroyed="taskDestroyed"
|
||||
/>
|
||||
<div class="row tasks-navigation">
|
||||
<div class="col-12 col-md-4">
|
||||
<h1>{{ $t('groupTasksTitle') }}</h1>
|
||||
<task-summary
|
||||
ref="taskSummary"
|
||||
:task="editingTask"
|
||||
@cancel="cancelTaskModal()"
|
||||
/>
|
||||
<div class="d-flex flex-wrap align-items-center mb-4">
|
||||
<div class="mr-auto">
|
||||
<h1>{{ group.name }}</h1>
|
||||
</div>
|
||||
<!-- @TODO: Abstract to component!-->
|
||||
<div class="col-12 col-md-4">
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="form-control input-search"
|
||||
type="text"
|
||||
:placeholder="$t('search')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="canCreateTasks"
|
||||
class="create-task-area d-flex"
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="form-control input-search"
|
||||
type="text"
|
||||
:placeholder="$t('search')"
|
||||
>
|
||||
<transition name="slide-tasks-btns">
|
||||
<div
|
||||
class="d-flex flex-wrap align-items-center justify-content-end ml-auto"
|
||||
>
|
||||
<toggle-switch
|
||||
id="taskMirrorToggle"
|
||||
class="mr-3 mb-1 ml-auto"
|
||||
:label="'Copy tasks'"
|
||||
:checked="user.preferences.tasks.mirrorGroupTasks.indexOf(group._id) !== -1"
|
||||
:hover-text="'Show assigned and open tasks on your personal task board'"
|
||||
@change="changeMirrorPreference"
|
||||
/>
|
||||
<div
|
||||
class="day-start d-flex align-items-center"
|
||||
v-html="$t('dayStart', { startTime: groupStartTime } )"
|
||||
>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<button
|
||||
id="create-task-btn"
|
||||
v-if="canCreateTasks"
|
||||
class="btn btn-primary create-btn d-flex align-items-center"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click.stop.prevent="openCreateBtn = !openCreateBtn"
|
||||
@keypress.enter="openCreateBtn = !openCreateBtn"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="svg-icon icon-10 color"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
|
||||
</button>
|
||||
<div
|
||||
v-if="openCreateBtn"
|
||||
class="d-flex"
|
||||
class="dropdown"
|
||||
>
|
||||
<div
|
||||
v-for="type in columns"
|
||||
:key="type"
|
||||
v-b-tooltip.hover.bottom="$t(type)"
|
||||
class="create-task-btn diamond-btn"
|
||||
@click="createTask(type)"
|
||||
class="dropdown-item d-flex px-2 py-1"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:class="`icon-${type}`"
|
||||
v-html="icons[type]"
|
||||
></div>
|
||||
<div class="d-flex align-items-center justify-content-center task-icon">
|
||||
<div
|
||||
class="svg-icon m-auto"
|
||||
:class="`icon-${type}`"
|
||||
v-html="icons[type]"
|
||||
></div>
|
||||
</div>
|
||||
<div class="task-label ml-2">
|
||||
{{ $t(type) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div
|
||||
id="create-task-btn"
|
||||
class="create-btn diamond-btn btn btn-success"
|
||||
:class="{open: openCreateBtn}"
|
||||
@click="openCreateBtn = !openCreateBtn"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
</div>
|
||||
<b-tooltip
|
||||
v-if="!openCreateBtn"
|
||||
target="create-task-btn"
|
||||
placement="bottom"
|
||||
>
|
||||
{{ $t('create') }}
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -79,6 +97,7 @@
|
||||
:search-text="searchText"
|
||||
:draggable-override="canCreateTasks"
|
||||
@editTask="editTask"
|
||||
@taskSummary="taskSummary"
|
||||
@loadGroupCompletedTodos="loadGroupCompletedTodos"
|
||||
@taskDestroyed="taskDestroyed"
|
||||
/>
|
||||
@@ -86,12 +105,58 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#taskMirrorToggle {
|
||||
font-weight: bold;
|
||||
|
||||
.svg-icon {
|
||||
margin: 3px 6px 0px 4px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.toggle-switch-description {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/create-task.scss';
|
||||
|
||||
.tasks-navigation {
|
||||
margin-bottom: 40px;
|
||||
h1 {
|
||||
color: $purple-300;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.day-start {
|
||||
height: 2rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 2px;
|
||||
color: $gray-100;
|
||||
background-color: $gray-600;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
.input-search {
|
||||
margin-left: 12.5rem;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.input-search {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
#create-task-btn {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.positive {
|
||||
@@ -107,11 +172,15 @@
|
||||
import Vue from 'vue';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import moment from 'moment';
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import TaskColumn from '../tasks/column';
|
||||
import TaskModal from '../tasks/taskModal';
|
||||
import TaskSummary from '../tasks/taskSummary';
|
||||
import GroupPlanOverviewModal from './groupPlanOverviewModal';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
|
||||
import sync from '../../mixins/sync';
|
||||
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import filterIcon from '@/assets/svg/filter.svg';
|
||||
@@ -121,14 +190,18 @@ import dailyIcon from '@/assets/svg/daily.svg';
|
||||
import todoIcon from '@/assets/svg/todo.svg';
|
||||
import rewardIcon from '@/assets/svg/reward.svg';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TaskColumn,
|
||||
TaskModal,
|
||||
TaskSummary,
|
||||
GroupPlanOverviewModal,
|
||||
toggleSwitch,
|
||||
},
|
||||
mixins: [sync],
|
||||
props: ['groupId'],
|
||||
data () {
|
||||
return {
|
||||
@@ -199,6 +272,16 @@ export default {
|
||||
return (this.group.leader && this.group.leader._id === this.user._id)
|
||||
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
|
||||
},
|
||||
groupStartTime () {
|
||||
if (!this.group || !this.group.cron) return null;
|
||||
const { dayStart, timezoneOffset } = this.group.cron;
|
||||
const timezoneDiff = this.user.preferences.timezoneOffset - timezoneOffset;
|
||||
return moment()
|
||||
.hour(dayStart)
|
||||
.minute(0)
|
||||
.subtract(timezoneDiff, 'minutes')
|
||||
.format('h:mm A');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes (when this route is already active)
|
||||
@@ -208,6 +291,10 @@ export default {
|
||||
this.$set(this, 'searchId', to.params.groupId);
|
||||
next();
|
||||
},
|
||||
async beforeRouteLeave (to, from, next) {
|
||||
await this.sync();
|
||||
next();
|
||||
},
|
||||
mounted () {
|
||||
if (!this.searchId) this.searchId = this.groupId;
|
||||
this.load();
|
||||
@@ -218,6 +305,7 @@ export default {
|
||||
|
||||
this.$root.$on('habitica:team-sync', () => {
|
||||
this.loadTasks();
|
||||
this.loadGroupCompletedTodos();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
@@ -238,6 +326,9 @@ export default {
|
||||
this.group.members = members;
|
||||
|
||||
this.loadTasks();
|
||||
if (this.user.flags.tour.groupPlans !== -2) {
|
||||
this.$root.$emit('bv::show::modal', 'group-plans-update');
|
||||
}
|
||||
},
|
||||
async loadTasks () {
|
||||
this.tasksByType = {
|
||||
@@ -251,22 +342,13 @@ export default {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
const groupedApprovals = await this.loadApprovals();
|
||||
|
||||
tasks.forEach(task => {
|
||||
if (
|
||||
groupedApprovals[task._id]
|
||||
&& groupedApprovals[task._id].length > 0
|
||||
) task.approvals = groupedApprovals[task._id];
|
||||
this.tasksByType[task.type].push(task);
|
||||
});
|
||||
},
|
||||
async loadApprovals () {
|
||||
const approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
return groupBy(approvalRequests, 'group.taskId');
|
||||
if (this.editingTask && this.editingTask.completed) {
|
||||
this.loadGroupCompletedTodos();
|
||||
}
|
||||
},
|
||||
editTask (task) {
|
||||
this.taskFormPurpose = 'edit';
|
||||
@@ -277,6 +359,12 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
taskSummary (task) {
|
||||
this.editingTask = cloneDeep(task);
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-summary');
|
||||
});
|
||||
},
|
||||
async loadGroupCompletedTodos () {
|
||||
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
|
||||
groupId: this.searchId,
|
||||
@@ -299,14 +387,6 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
taskCreated (task) {
|
||||
task.group.id = this.group._id;
|
||||
this.tasksByType[task.type].unshift(task);
|
||||
},
|
||||
taskEdited (task) {
|
||||
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
|
||||
this.tasksByType[task.type].splice(index, 1, task);
|
||||
},
|
||||
taskDestroyed (task) {
|
||||
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
|
||||
this.tasksByType[task.type].splice(index, 1);
|
||||
@@ -354,6 +434,25 @@ export default {
|
||||
if (this.temporarilySelectedTags.indexOf(tagId) !== -1) return true;
|
||||
return false;
|
||||
},
|
||||
changeMirrorPreference (newVal) {
|
||||
Analytics.track({
|
||||
eventName: 'mirror tasks',
|
||||
eventAction: 'mirror tasks',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
mirror: newVal,
|
||||
group: this.group._id,
|
||||
}, { trackOnClient: true });
|
||||
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
|
||||
if (newVal) { // we're turning copy ON for this group
|
||||
groupsToMirror.push(this.group._id);
|
||||
} else { // we're turning copy OFF for this group
|
||||
groupsToMirror.splice(groupsToMirror.indexOf(this.group._id), 1);
|
||||
}
|
||||
this.$store.dispatch('user:set', {
|
||||
'preferences.tasks.mirrorGroupTasks': groupsToMirror,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 || isUserAdmin;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -114,15 +111,20 @@ export function canEdit (store) {
|
||||
};
|
||||
}
|
||||
|
||||
function _nonInteractive (task) {
|
||||
return (task.group && task.group.id && !task.userId)
|
||||
|| (task.challenge && task.challenge.id && !task.userId)
|
||||
|| (task.group && task.group.approval && task.group.approval.requested
|
||||
&& task.type !== 'habit');
|
||||
function _nonInteractive (task, userId) {
|
||||
if (task.userId) return false;
|
||||
if (task.challenge && task.challenge.id) return true;
|
||||
if (
|
||||
task.group && task.group.assignedUsers
|
||||
&& task.group.assignedUsers.length > 0
|
||||
&& task.group.assignedUsers.indexOf(userId) === -1
|
||||
) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getTaskClasses (store) {
|
||||
const userPreferences = store.state.user.data.preferences;
|
||||
const userId = store.state.user.data._id;
|
||||
|
||||
// Purpose can be one of the following strings:
|
||||
// Edit Modal: edit-modal-bg, edit-modal-text, edit-modal-icon
|
||||
@@ -169,9 +171,14 @@ export function getTaskClasses (store) {
|
||||
|
||||
case 'control':
|
||||
if (type === 'todo' || type === 'daily') {
|
||||
if (task.completed || (!shouldDo(dueDate, task, userPreferences) && type === 'daily')) {
|
||||
if (task.completed
|
||||
|| (!shouldDo(dueDate, task, userPreferences) && type === 'daily')
|
||||
|| (task.group && task.group.assignedUsersDetail
|
||||
&& task.group.assignedUsersDetail[userId]
|
||||
&& task.group.assignedUsersDetail[userId].completed)
|
||||
) {
|
||||
return {
|
||||
bg: _nonInteractive(task) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg',
|
||||
bg: _nonInteractive(task, userId) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg',
|
||||
checkbox: 'task-disabled-daily-todo-control-checkbox',
|
||||
inner: 'task-disabled-daily-todo-control-inner',
|
||||
content: 'task-disabled-daily-todo-control-content',
|
||||
@@ -179,28 +186,28 @@ export function getTaskClasses (store) {
|
||||
}
|
||||
|
||||
return {
|
||||
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
checkbox: `task-${color}-control-checkbox`,
|
||||
inner: `task-${color}-control-inner-daily-todo`,
|
||||
icon: `task-${color}-control-icon`,
|
||||
};
|
||||
} if (type === 'reward') {
|
||||
return {
|
||||
bg: _nonInteractive(task) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg',
|
||||
bg: _nonInteractive(task, userId) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg',
|
||||
};
|
||||
} if (type === 'habit') {
|
||||
return {
|
||||
up: task.up
|
||||
? {
|
||||
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
icon: `task-${color}-control-icon`,
|
||||
}
|
||||
: { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
|
||||
down: task.down
|
||||
? {
|
||||
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
|
||||
icon: `task-${color}-control-icon`,
|
||||
}
|
||||
: { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
|
||||
|
||||
@@ -35,7 +35,10 @@ describe('canEdit getter', () => {
|
||||
});
|
||||
it('can Edit task in own dashboard', () => {
|
||||
expect(store.getters['tasks:canEdit'](task, 'challenge', true, null, challenge)).to.equal(true);
|
||||
expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(true);
|
||||
});
|
||||
|
||||
it('cannot Edit group task in own dashboard', () => {
|
||||
expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(false);
|
||||
});
|
||||
|
||||
it('can Edit any challenge task if admin', () => {
|
||||
|
||||
@@ -143,7 +143,14 @@ describe('getTaskClasses getter', () => {
|
||||
});
|
||||
|
||||
it('returns noninteractive classes and padlock icons for group board tasks', () => {
|
||||
const task = { type: 'todo', value: 2, group: { id: 'group-id' } };
|
||||
const task = {
|
||||
type: 'todo',
|
||||
value: 2,
|
||||
group: {
|
||||
id: 'group-id',
|
||||
assignedUsers: ['not-me'],
|
||||
},
|
||||
};
|
||||
expect(getTaskClasses(task, 'control')).to.deep.equal({
|
||||
bg: 'task-good-control-bg-noninteractive',
|
||||
checkbox: 'task-good-control-checkbox',
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"hatchingPotionSolarSystem": "Solar System",
|
||||
"hatchingPotionOnyx": "Onyx",
|
||||
"hatchingPotionVirtualPet": "Virtual Pet",
|
||||
"hatchingPotionPorcelain": "Porcelain",
|
||||
|
||||
"hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText(locale) %> pet.",
|
||||
"premiumPotionAddlNotes": "Not usable on quest pet eggs. Available for purchase until <%= date(locale) %>.",
|
||||
|
||||
@@ -68,5 +68,8 @@
|
||||
|
||||
"iosFaqStillNeedHelp": "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 Tavern chat under Menu > Tavern! We're happy to help.",
|
||||
"androidFaqStillNeedHelp": "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 Tavern chat under Menu > Tavern! We're happy to help.",
|
||||
"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 [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! We're happy to help."
|
||||
"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 [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! We're happy to help.",
|
||||
|
||||
"faqQuestion13": "What is a Group Plan?",
|
||||
"webFaqAnswer13": "## How do Group Plans work?\n\nA [Group Plan](/group-plans) gives your Party or Guild access to a shared task board that’s similar to your personal task board! It’s a shared Habitica experience where tasks can be created and checked off by anyone in the group.\n\nThere are also features available like member roles, status view, and task assigning that give you a more controlled experience. [Visit our wiki](https://habitica.fandom.com/wiki/Group_Plans) to learn more about our Group Plans’ features!\n\n## Who benefits from a Group Plan?\n\nGroup Plans work best when you have a small team of people who want to collaborate together. We recommend 2-5 members.\n\nGroup Plans are great for families, whether it’s a parent and child or you and a partner. Shared goals, chores, or responsibilities are easy to keep track of on one board.\n\nGroup Plans can also be useful for teams of colleagues that have shared goals, or managers that want to introduce their employees to gamification.\n\n## Quick tips for using Groups\n\nHere are some quick tips to get you started with your new Group. We’ll provide more details in the following sections:\n\n* Make a member a manager to give them the ability to create and edit tasks\n* Leave tasks unassigned if anyone can complete it and it only needs done once\n* Assign a task to one person to make sure no one else can complete their task\n* Assign a task to multiple people if they all need to complete it\n* Toggle the ability to display shared tasks on your personal board to not miss anything\n* You get rewarded for the tasks you complete, even multi-assigned\n* Task completion rewards aren’t shared or split between Team members\n* Use task color on the team board to judge the average completion rate of tasks\n* Regularly review the tasks on your Team Board to make sure they are still relevant\n* Missing a Daily won’t damage you or your team, but the task will degrade in color\n\n## How can others in the group create tasks?\n\nOnly the group leader and managers can create tasks. If you’d like a group member to be able to create tasks, then you should promote them to be a manager by going to the Group Information tab, viewing the member list, and clicking the dot icon by their name.\n\n## How does assigning a task work?\n\nGroup Plans give you the unique ability to assign tasks to other group members. Assigning a task is great for delegating. If you assign a task to someone, then other members are prevented from completing it.\n\nYou can also assign a task to multiple people if it needs to be completed by more than one member. For example, if everyone has to brush their teeth, create a task and assign it to each group member. They will all be able to check it off and get their individual rewards for doing so. The main task will show as complete once everyone checks it off.\n\n## How do unassigned tasks work?\n\nUnassigned tasks can be completed by anyone in the group, so leave a task unassigned to allow any member to complete it. For example, taking out the trash. Whoever takes out the trash can check off the unassigned task and it will show as completed for everyone.\n\n## How does the synchronized day reset work?\n\nShared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the group leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in color to help visualize progress. We don’t want the shared experience to be a negative one!\n\n## How do I use my Group on the mobile apps?\n\nWhile the mobile apps don’t fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android app. On the browser version of Habitica, go to your group’s shared task board and turn on the copy tasks toggle. Now all open and assigned shared tasks will display on your personal task board across all platforms.\n\n## What’s the difference between a Group’s shared tasks and Challenges?\n\nGroup Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction."
|
||||
}
|
||||
|
||||
@@ -471,6 +471,8 @@
|
||||
"weaponMystery202111Notes": "Shape the flow of time with this mysterious and powerful staff. Confers no benefit. November 2021 Subscriber Item.",
|
||||
"weaponMystery202201Text": "Midnight Confetti Cannon",
|
||||
"weaponMystery202201Notes": "Unleash a cloud of gold and silver glitter when the clock strikes midnight. Happy New Year! Now who's cleaning this up? Confers no benefit. January 2022 Subscriber Item.",
|
||||
"weaponMystery202209Text": "Magic Manual",
|
||||
"weaponMystery202209Notes": "This book will guide you through your journey into magic-making. Confers no benefit. September 2022 Subscriber Item.",
|
||||
"weaponMystery301404Text": "Steampunk Cane",
|
||||
"weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.",
|
||||
|
||||
@@ -2357,6 +2359,8 @@
|
||||
"shieldMystery201902Notes": "This glittery paper forms magic hearts that slowly drift and dance in the air. Confers no benefit. February 2019 Subscriber Item.",
|
||||
"shieldMystery202011Text": "Foliated Staff",
|
||||
"shieldMystery202011Notes": "Harness the power of the autumn wind with this staff. Use for arcane magic or to make awesome leaf piles, the choice is yours! Confers no benefit. November 2020 Subscriber Item.",
|
||||
"shieldMystery202209Text": "Mound o' Magic Books",
|
||||
"shieldMystery202209Notes": "Building your sorcery knowledge takes a lot of reading, but you're sure to enjoy your education. Confers no benefit. September 2022 Subscriber Item.",
|
||||
"shieldMystery301405Text": "Clock Shield",
|
||||
"shieldMystery301405Notes": "Time is on your side with this towering clock shield! Confers no benefit. June 3015 Subscriber Item.",
|
||||
"shieldMystery301704Text": "Fluttery Fan",
|
||||
|
||||
@@ -175,14 +175,15 @@
|
||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
||||
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
||||
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
|
||||
"assignedTo": "Assign To",
|
||||
"assignedToUser": "Assigned to <strong><%- userName %></strong>",
|
||||
"assignedToMembers": "Assigned to <strong><%= userCount %> members</strong>",
|
||||
"assignedToYouAndMembers": "Assigned to you and <strong><%= userCount %> members</strong>",
|
||||
"youAreAssigned": "Assigned to you",
|
||||
"assignTo": "Assign To",
|
||||
"assignedTo": "Assigned to",
|
||||
"assignedToUser": "Assigned: <strong>@<%- userName %></strong>",
|
||||
"assignedToMembers": "<%= userCount %> users",
|
||||
"assignedToYouAndMembers": "<strong>You</strong>, <%= userCount %> users",
|
||||
"youAreAssigned": "Assigned: <strong>you</strong>",
|
||||
"taskIsUnassigned": "This task is unassigned",
|
||||
"unassigned": "Unassigned",
|
||||
"chooseTeamMember": "Choose a team member",
|
||||
"chooseTeamMember": "Search for a team member",
|
||||
"confirmUnClaim": "Are you sure you want to unclaim this task?",
|
||||
"confirmNeedsWork": "Are you sure you want to mark this task as needing work?",
|
||||
"userRequestsApproval": "<strong><%- userName %></strong> requests approval",
|
||||
@@ -200,7 +201,7 @@
|
||||
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green notification-bold\"><%- taskText %></span> has been approved.",
|
||||
"thisTaskApproved": "This task was approved",
|
||||
"taskClaimed": "<%- userName %> has claimed the task <span class=\"notification-bold\"><%- taskText %></span>.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%- managerName %></span> marked <span class=\"notification-bold\"><%- taskText %></span> as needing additional work.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%- taskText %></span> was unchecked by <span class=\"notification-bold\">@<%- managerName %></span>. Your rewards for completing the task were reverted.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%- user %></span> requests approval for <span class=\"notification-bold\"><%- taskName %></span>",
|
||||
"approve": "Approve",
|
||||
"approveTask": "Approve Task",
|
||||
@@ -363,7 +364,28 @@
|
||||
"groupActivityNotificationTitle": "<%= user %> posted in <%= group %>",
|
||||
"managerNotes": "Manager's Notes",
|
||||
"assignedDateOnly": "Assigned on <strong><%= date %></strong>",
|
||||
"assignedDateAndUser": "Assigned by <strong>@<%- username %></strong> on <strong><%= date %></strong>",
|
||||
"assignedDateAndUser": "Assigned by @<%- username %> on <%= date %>",
|
||||
"claimRewards": "Claim Rewards",
|
||||
"chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later."
|
||||
"dayStart": "<strong>Day start</strong>: <%= startTime %>",
|
||||
"viewStatus": "Status",
|
||||
"lastCompleted": "Last completed",
|
||||
"youEmphasized": "<strong>You</strong>",
|
||||
"chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later.",
|
||||
"newGroupsWelcome": "Welcome to the New Shared Task Board!",
|
||||
"newGroupsWhatsNew": "Check Out What's New:",
|
||||
"newGroupsBullet01": "Interact with tasks directly from the shared task board",
|
||||
"newGroupsBullet02": "Anyone can complete an unassigned task",
|
||||
"newGroupsBullet03": "Shared tasks reset at the same time for everyone for easier collaboration",
|
||||
"newGroupsBullet04": "Shared Dailies will not cause damage when missed or appear in the Record Yesterday’s Activity prompt",
|
||||
"newGroupsBullet05": "Shared tasks will degrade in color if left incomplete to help track progress",
|
||||
"newGroupsBullet06": "The task status view allows you to quickly see which assignee has completed a task",
|
||||
"newGroupsBullet07": "Toggle the ability to display the shared tasks on your personal task board",
|
||||
"newGroupsBullet08": "The group leader and managers can quickly add tasks from the top of the task columns",
|
||||
"newGroupsBullet09": "A shared task can be unchecked to show it still needs work",
|
||||
"newGroupsBullet10": "Assignment status determines completion condition:",
|
||||
"newGroupsBullet10a": "<strong>Leave a task unassigned</strong> if any member can complete it",
|
||||
"newGroupsBullet10b": "<strong>Assign a task to one member</strong> so only they can complete it",
|
||||
"newGroupsBullet10c": "<strong>Assign a task to multiple members</strong> if they all need to complete it",
|
||||
"newGroupsVisitFAQ": "Visit the <a href='/static/faq#group-plans' target='_blank'>FAQ</a> from the Help dropdown for more guidance.",
|
||||
"newGroupsEnjoy": "We hope you enjoy the new Group Plans experience!"
|
||||
}
|
||||
|
||||
@@ -17,10 +17,9 @@
|
||||
"mattBochText1": "Welcome to the Stable! I’m Matt, the beastmaster. Every time you complete a task, you'll have a random chance at receiving an Egg or a Hatching Potion to hatch Pets. When you hatch a Pet, it will appear here! Click a Pet's image to add it to your Avatar. Feed them with the Pet Food you find, and they'll grow into hardy Mounts.",
|
||||
"welcomeToTavern": "Welcome to The Tavern!",
|
||||
"sleepDescription": "Need a break? Check into Daniel's Inn to pause some of Habitica's more difficult game mechanics:",
|
||||
"sleepBullet1": "Missed Dailies won't damage you",
|
||||
"sleepBullet2": "Tasks won't lose streaks",
|
||||
"sleepBullet3": "Bosses won't do damage for your own missed Dailies",
|
||||
"sleepBullet4": "Your boss damage or collection Quest items will stay pending until check-out",
|
||||
"sleepBullet1": "Your missed Dailies won't damage you (bosses will still do damage caused by other Party member's missed Dailies)",
|
||||
"sleepBullet2": "Your Task streaks and Habit counters will not reset",
|
||||
"sleepBullet3": "Your damage to the Quest boss or found collection items will remain pending until you check out of the Inn",
|
||||
"pauseDailies": "Pause Damage",
|
||||
"unpauseDailies": "Unpause Damage",
|
||||
"staffAndModerators": "Staff and Moderators",
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"mysterySet202206": "Sea Sprite Set",
|
||||
"mysterySet202207": "Jammin' Jelly Set",
|
||||
"mysterySet202208": "Perky Ponytail Set",
|
||||
"mysterySet202209": "Magical Scholar Set",
|
||||
"mysterySet301404": "Steampunk Standard Set",
|
||||
"mysterySet301405": "Steampunk Accessories Set",
|
||||
"mysterySet301703": "Peacock Steampunk Set",
|
||||
|
||||
@@ -132,5 +132,6 @@
|
||||
"errorTemporaryItem": "This item is temporary and cannot be pinned.",
|
||||
"addTags": "Add tags...",
|
||||
"enterTag": "Enter a tag",
|
||||
"pressEnterToAddTag": "Press Enter to add tag: '<%= tagName %>'"
|
||||
"pressEnterToAddTag": "Press Enter to add tag: '<%= tagName %>'",
|
||||
"taskSummary": "<%= type %> Summary"
|
||||
}
|
||||
|
||||
@@ -9,12 +9,18 @@ const gemsPromo = {
|
||||
};
|
||||
|
||||
export const EVENTS = {
|
||||
noCurrentEventAfter: {
|
||||
start: '2022-09-30T20:00-04:00',
|
||||
noCurrentEvent: {
|
||||
start: '2022-08-31T20:00-04:00',
|
||||
end: '2022-12-21T08:00-04:00',
|
||||
season: 'normal',
|
||||
npcImageSuffix: '',
|
||||
},
|
||||
potions202208: {
|
||||
start: '2022-08-16T08:00-04:00',
|
||||
end: '2022-08-31T20:00-04:00',
|
||||
season: 'normal',
|
||||
npcImageSuffix: '',
|
||||
},
|
||||
bundle202208: {
|
||||
start: '2022-08-09T08:00-04:00',
|
||||
end: '2022-09-30T20:00-04:00',
|
||||
@@ -28,12 +34,6 @@ export const EVENTS = {
|
||||
npcImageSuffix: '_summer',
|
||||
gear: true,
|
||||
},
|
||||
noCurrentEvent: {
|
||||
start: '2022-04-30T20:00-04:00',
|
||||
end: '2022-06-21T08:00-04:00',
|
||||
season: 'normal',
|
||||
npcImageSuffix: '',
|
||||
},
|
||||
bundle202206: {
|
||||
start:'2022-06-14T08:00-04:00',
|
||||
end:'2022-06-30T20:00-04:00',
|
||||
|
||||
@@ -231,6 +231,7 @@ const shield = {
|
||||
201802: { },
|
||||
201902: { },
|
||||
202011: { },
|
||||
202209: { },
|
||||
301405: { },
|
||||
301704: { },
|
||||
};
|
||||
@@ -248,6 +249,7 @@ const weapon = {
|
||||
202104: { twoHanded: true },
|
||||
202111: { twoHanded: true },
|
||||
202201: { },
|
||||
202209: { },
|
||||
301404: { },
|
||||
};
|
||||
|
||||
|
||||
@@ -503,12 +503,13 @@ const premium = {
|
||||
value: 2,
|
||||
text: t('hatchingPotionMoonglow'),
|
||||
limited: true,
|
||||
event: EVENTS.potions202108,
|
||||
event: EVENTS.potions202208,
|
||||
_addlNotes: t('premiumPotionAddlNotes', {
|
||||
date: t('dateEndAugust'),
|
||||
previousDate: t('augustYYYY', { year: 2021 }),
|
||||
}),
|
||||
canBuy () {
|
||||
return moment().isBetween(EVENTS.potions202108.start, EVENTS.potions202108.end);
|
||||
return moment().isBetween(EVENTS.potions202208.start, EVENTS.potions202208.end);
|
||||
},
|
||||
},
|
||||
SolarSystem: {
|
||||
@@ -525,6 +526,18 @@ const premium = {
|
||||
canBuy: hasQuestAchievementFunction('onyx'),
|
||||
_addlNotes: t('premiumPotionUnlimitedNotes'),
|
||||
},
|
||||
Porcelain: {
|
||||
value: 2,
|
||||
text: t('hatchingPotionPorcelain'),
|
||||
limited: true,
|
||||
event: EVENTS.potions202208,
|
||||
_addlNotes: t('premiumPotionAddlNotes', {
|
||||
date: t('dateEndAugust'),
|
||||
}),
|
||||
canBuy () {
|
||||
return moment().isBetween(EVENTS.potions202208.start, EVENTS.potions202208.end);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wacky = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EVENTS } from './constants';
|
||||
// path: 'premiumHatchingPotions.Rainbow',
|
||||
const featuredItems = {
|
||||
market () {
|
||||
if (moment().isBefore(EVENTS.summer2022.end)) {
|
||||
if (moment().isBetween(EVENTS.potions202208.start, EVENTS.potions202208.end)) {
|
||||
return [
|
||||
{
|
||||
type: 'armoire',
|
||||
@@ -13,15 +13,15 @@ const featuredItems = {
|
||||
},
|
||||
{
|
||||
type: 'premiumHatchingPotion',
|
||||
path: 'premiumHatchingPotions.Sunset',
|
||||
path: 'premiumHatchingPotions.Moonglow',
|
||||
},
|
||||
{
|
||||
type: 'premiumHatchingPotion',
|
||||
path: 'premiumHatchingPotions.Watery',
|
||||
path: 'premiumHatchingPotions.Porcelain',
|
||||
},
|
||||
{
|
||||
type: 'premiumHatchingPotion',
|
||||
path: 'premiumHatchingPotions.Aquatic',
|
||||
type: 'food',
|
||||
path: 'food.Milk',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -32,15 +32,15 @@ const featuredItems = {
|
||||
},
|
||||
{
|
||||
type: 'food',
|
||||
path: 'food.Honey',
|
||||
path: 'food.RottenMeat',
|
||||
},
|
||||
{
|
||||
type: 'hatchingPotions',
|
||||
path: 'hatchingPotions.CottonCandyPink',
|
||||
path: 'hatchingPotions.Zombie',
|
||||
},
|
||||
{
|
||||
type: 'eggs',
|
||||
path: 'eggs.BearCub',
|
||||
path: 'eggs.Dragon',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import find from 'lodash/find';
|
||||
import timesLodash from 'lodash/times';
|
||||
import reduce from 'lodash/reduce';
|
||||
import moment from 'moment';
|
||||
import max from 'lodash/max';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../libs/errors';
|
||||
import i18n from '../i18n';
|
||||
@@ -104,6 +106,7 @@ function _gainMP (user, val) {
|
||||
// ===== CONSTITUTION =====
|
||||
// TODO Decreases HP loss from bad habits / missed dailies by 0.5% per point.
|
||||
function _subtractPoints (user, task, stats, delta) {
|
||||
if (task.group.id && task.type === 'daily') return stats.hp;
|
||||
let conBonus = 1 - statsComputed(user).con / 250;
|
||||
if (conBonus < 0.1) conBonus = 0.1;
|
||||
|
||||
@@ -233,11 +236,6 @@ export default function scoreTask (options = {}, req = {}, analytics) {
|
||||
exp: user.stats.exp,
|
||||
};
|
||||
|
||||
if (
|
||||
task.group && task.group.approval && task.group.approval.required
|
||||
&& !task.group.approval.approved && !(task.type === 'todo' && cron)
|
||||
) return 0;
|
||||
|
||||
// This is for setting one-time temporary flags,
|
||||
// such as streakBonus or itemDropped. Useful for notifying
|
||||
// the API consumer, then cleared afterwards
|
||||
@@ -248,6 +246,13 @@ export default function scoreTask (options = {}, req = {}, analytics) {
|
||||
|
||||
if (oldLeveledUp) user._tmp.leveledUp = oldLeveledUp;
|
||||
|
||||
// Thanks to open group tasks, userId is not guaranteed. Don't allow scoring inaccessible tasks
|
||||
if (task.userId && task.userId !== user._id) {
|
||||
throw new BadRequest('Cannot score task belonging to another user.');
|
||||
} else if (task.group.id && user.guilds.indexOf(task.group.id) === -1
|
||||
&& user.party._id !== task.group.id) {
|
||||
throw new BadRequest('Cannot score task belonging to another user.');
|
||||
}
|
||||
// If they're trying to purchase a too-expensive reward, don't allow them to do that.
|
||||
if (task.value > user.stats.gp && task.type === 'reward') throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
|
||||
@@ -296,34 +301,69 @@ export default function scoreTask (options = {}, req = {}, analytics) {
|
||||
_gainMP(user, max([1, 0.01 * statsComputed(user).maxMP]) * (direction === 'down' ? -1 : 1));
|
||||
|
||||
if (direction === 'up') {
|
||||
task.streak += 1;
|
||||
// Give a streak achievement when the streak is a multiple of 21
|
||||
if (task.streak !== 0 && task.streak % 21 === 0) {
|
||||
user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
|
||||
if (user.addNotification) user.addNotification('STREAK_ACHIEVEMENT');
|
||||
}
|
||||
task.completed = true;
|
||||
if (task.group.id) {
|
||||
if (!task.group.assignedUsers || task.group.assignedUsers.length === 0) {
|
||||
task.group.completedBy = {
|
||||
userId: user._id,
|
||||
date: new Date(),
|
||||
};
|
||||
task.completed = true;
|
||||
task.streak += 1;
|
||||
} else {
|
||||
task.group.assignedUsersDetail[user._id].completed = true;
|
||||
task.group.assignedUsersDetail[user._id].completedDate = new Date();
|
||||
if (!find(task.group.assignedUsersDetail, assignedUser => !assignedUser.completed)) {
|
||||
task.dateCompleted = new Date();
|
||||
task.completed = true;
|
||||
task.streak += 1;
|
||||
}
|
||||
}
|
||||
if (task.markModified) task.markModified('group');
|
||||
} else {
|
||||
task.streak += 1;
|
||||
// Give a streak achievement when the streak is a multiple of 21
|
||||
if (task.streak !== 0 && task.streak % 21 === 0) {
|
||||
user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
|
||||
if (user.addNotification) user.addNotification('STREAK_ACHIEVEMENT');
|
||||
}
|
||||
task.completed = true;
|
||||
|
||||
// Save history entry for daily
|
||||
task.history = task.history || [];
|
||||
const historyEntry = {
|
||||
date: Number(new Date()),
|
||||
value: task.value,
|
||||
isDue: task.isDue,
|
||||
completed: true,
|
||||
};
|
||||
task.history.push(historyEntry);
|
||||
// Save history entry for daily
|
||||
task.history = task.history || [];
|
||||
const historyEntry = {
|
||||
date: Number(new Date()),
|
||||
value: task.value,
|
||||
isDue: task.isDue,
|
||||
completed: true,
|
||||
};
|
||||
task.history.push(historyEntry);
|
||||
}
|
||||
} else if (direction === 'down') {
|
||||
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone
|
||||
if (task.streak !== 0 && task.streak % 21 === 0) {
|
||||
user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
|
||||
}
|
||||
task.streak -= 1;
|
||||
task.completed = false;
|
||||
if (task.group.id) {
|
||||
if (!task.group.assignedUsersDetail
|
||||
|| !find(task.group.assignedUsersDetail, assignedUser => !assignedUser.completed)
|
||||
) {
|
||||
task.streak -= 1;
|
||||
task.completed = false;
|
||||
}
|
||||
if (task.group.completedBy) task.group.completedBy = {};
|
||||
if (task.group.assignedUsersDetail && task.group.assignedUsersDetail[user._id]) {
|
||||
task.group.assignedUsersDetail[user._id].completed = false;
|
||||
task.group.assignedUsersDetail[user._id].completedDate = undefined;
|
||||
}
|
||||
if (task.markModified) task.markModified('group');
|
||||
} else {
|
||||
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone
|
||||
if (task.streak !== 0 && task.streak % 21 === 0) {
|
||||
user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
|
||||
}
|
||||
task.streak -= 1;
|
||||
task.completed = false;
|
||||
|
||||
// Delete history entry when daily unchecked
|
||||
if (task.history || task.history.length > 0) {
|
||||
task.history.splice(-1, 1);
|
||||
// Delete history entry when daily unchecked
|
||||
if (task.history || task.history.length > 0) {
|
||||
task.history.splice(-1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,11 +372,37 @@ export default function scoreTask (options = {}, req = {}, analytics) {
|
||||
delta += _changeTaskValue(user, task, direction, times, cron);
|
||||
} else {
|
||||
if (direction === 'up') {
|
||||
task.dateCompleted = new Date();
|
||||
task.completed = true;
|
||||
if (task.group.id) {
|
||||
if (!task.group.assignedUsers || task.group.assignedUsers.length === 0) {
|
||||
task.group.completedBy = {
|
||||
userId: user._id,
|
||||
date: new Date(),
|
||||
};
|
||||
task.completed = true;
|
||||
} else {
|
||||
task.group.assignedUsersDetail[user._id].completed = true;
|
||||
task.group.assignedUsersDetail[user._id].completedDate = new Date();
|
||||
if (!find(task.group.assignedUsersDetail, assignedUser => !assignedUser.completed)) {
|
||||
task.dateCompleted = new Date();
|
||||
task.completed = true;
|
||||
}
|
||||
}
|
||||
if (task.markModified) task.markModified('group');
|
||||
} else {
|
||||
task.dateCompleted = new Date();
|
||||
task.completed = true;
|
||||
}
|
||||
} else if (direction === 'down') {
|
||||
task.completed = false;
|
||||
task.dateCompleted = undefined;
|
||||
if (task.group.id) {
|
||||
if (task.group.completedBy) task.group.completedBy = {};
|
||||
if (task.group.assignedUsersDetail && task.group.assignedUsersDetail[user._id]) {
|
||||
task.group.assignedUsersDetail[user._id].completed = false;
|
||||
task.group.assignedUsersDetail[user._id].completedDate = undefined;
|
||||
}
|
||||
if (task.markModified) task.markModified('group');
|
||||
}
|
||||
}
|
||||
|
||||
delta += _changeTaskValue(user, task, direction, times, cron);
|
||||
|
||||
@@ -419,14 +419,16 @@ api.getGroup = {
|
||||
}
|
||||
|
||||
const groupJson = await Group.toJSONCleanChat(group, user);
|
||||
|
||||
if (groupJson.leader === user._id) {
|
||||
groupJson.purchased.plan = group.purchased.plan.toObject();
|
||||
}
|
||||
groupJson.purchased.plan = group.purchased.plan.toObject();
|
||||
|
||||
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||
const leader = await User.findById(groupJson.leader).select(nameFields).exec();
|
||||
const leader = await User.findById(groupJson.leader).select(`${nameFields} preferences.timezoneOffset preferences.dayStart`).exec();
|
||||
if (leader) groupJson.leader = leader.toJSON({ minimize: true });
|
||||
if (groupJson.purchased.plan.planId) {
|
||||
groupJson.cron.timezoneOffset = leader.preferences.timezoneOffset;
|
||||
groupJson.cron.dayStart = leader.preferences.dayStart;
|
||||
}
|
||||
delete groupJson.leader.preferences;
|
||||
|
||||
res.respond(200, groupJson);
|
||||
},
|
||||
|
||||
@@ -632,7 +632,6 @@ api.updateTask = {
|
||||
verifyTaskModification(task, user, group, challenge, res);
|
||||
}
|
||||
|
||||
const oldCheckList = task.checklist;
|
||||
// we have to convert task to an object because otherwise things
|
||||
// don't get merged correctly. Bad for performances?
|
||||
const [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
|
||||
@@ -654,14 +653,7 @@ api.updateTask = {
|
||||
// the other of the keys when using .toObject()
|
||||
// see https://github.com/Automattic/mongoose/issues/2749
|
||||
|
||||
task.group.approval.required = false;
|
||||
if (sanitizedObj.requiresApproval) {
|
||||
task.group.approval.required = true;
|
||||
}
|
||||
if (sanitizedObj.sharedCompletion) {
|
||||
task.group.sharedCompletion = sanitizedObj.sharedCompletion;
|
||||
}
|
||||
if (sanitizedObj.managerNotes) {
|
||||
if (Object.prototype.hasOwnProperty.call(sanitizedObj, 'managerNotes')) {
|
||||
task.group.managerNotes = sanitizedObj.managerNotes;
|
||||
}
|
||||
|
||||
@@ -695,28 +687,26 @@ api.updateTask = {
|
||||
setNextDue(task, user);
|
||||
const savedTask = await task.save();
|
||||
|
||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||
const updateCheckListItems = _.remove(sanitizedObj.checklist, checklist => {
|
||||
const indexOld = _.findIndex(oldCheckList, check => check.id === checklist.id);
|
||||
if (indexOld !== -1) return checklist.text !== oldCheckList[indexOld].text;
|
||||
return false; // Only return changes. Adding and remove are handled differently
|
||||
});
|
||||
|
||||
await group.updateTask(savedTask, { updateCheckListItems });
|
||||
}
|
||||
|
||||
res.respond(200, savedTask);
|
||||
|
||||
if (challenge) {
|
||||
challenge.updateTask(savedTask);
|
||||
} else if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||
await group.updateTask(savedTask);
|
||||
} else {
|
||||
} else if (!group) {
|
||||
taskActivityWebhook.send(user, {
|
||||
type: 'updated',
|
||||
task: savedTask,
|
||||
});
|
||||
}
|
||||
|
||||
if (group) {
|
||||
res.analytics.track('task edit', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
taskType: task.type,
|
||||
groupID: group._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -772,17 +762,12 @@ api.scoreTask = {
|
||||
|
||||
const userStats = user.stats.toJSON();
|
||||
|
||||
// group tasks that require a manager's approval
|
||||
if (taskResponse.requiresApproval === true) {
|
||||
res.respond(202, { requiresApproval: true }, taskResponse.message);
|
||||
} else {
|
||||
const resJsonData = _.assign({
|
||||
delta: taskResponse.delta,
|
||||
_tmp: user._tmp,
|
||||
}, userStats);
|
||||
const resJsonData = _.assign({
|
||||
delta: taskResponse.delta,
|
||||
_tmp: user._tmp,
|
||||
}, userStats);
|
||||
|
||||
res.respond(200, resJsonData);
|
||||
}
|
||||
res.respond(200, resJsonData);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -830,40 +815,25 @@ api.moveTask = {
|
||||
|
||||
const group = await getGroupFromTaskAndUser(task, user);
|
||||
const challenge = await getChallengeFromTask(task);
|
||||
verifyTaskModification(task, user, group, challenge, res);
|
||||
if (task.group.id && !task.userId) {
|
||||
if (!group || (user.guilds.indexOf(group._id) === -1 && user.party._id !== group._id)) {
|
||||
throw new NotFound(res.t('groupNotFound'));
|
||||
}
|
||||
if (task.group.assignedUsers.length !== 0
|
||||
&& task.group.assignedUsers.indexOf(user._id) === -1) {
|
||||
throw new BadRequest('Use /group/:groupId/tasks/:taskId/move/to/:position route');
|
||||
}
|
||||
} else {
|
||||
verifyTaskModification(task, user, group, challenge, res);
|
||||
}
|
||||
|
||||
if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo'));
|
||||
|
||||
const owner = group || challenge || user;
|
||||
const owner = challenge || user;
|
||||
|
||||
// In memory updates
|
||||
const order = owner.tasksOrder[`${task.type}s`];
|
||||
|
||||
if (order.indexOf(task._id) === -1) { // task is missing from list, list needs repair
|
||||
const taskListQuery = { type: task.type };
|
||||
if (group) {
|
||||
taskListQuery['group.id'] = owner._id;
|
||||
taskListQuery.userId = { $exists: false };
|
||||
} else if (challenge) {
|
||||
taskListQuery['challenge.id'] = owner._id;
|
||||
taskListQuery.userId = { $exists: false };
|
||||
} else {
|
||||
taskListQuery.userId = owner._id;
|
||||
}
|
||||
const taskList = await Tasks.Task.find(
|
||||
taskListQuery,
|
||||
{ _id: 1 },
|
||||
).exec();
|
||||
for (const foundTask of taskList) {
|
||||
if (order.indexOf(foundTask._id) === -1) {
|
||||
order.push(foundTask._id);
|
||||
}
|
||||
}
|
||||
const fixQuery = { $set: {} };
|
||||
fixQuery.$set[`tasksOrder.${task.type}s`] = order;
|
||||
await owner.update(fixQuery).exec();
|
||||
}
|
||||
|
||||
moveTask(order, task._id, to);
|
||||
|
||||
// Server updates
|
||||
@@ -886,7 +856,7 @@ api.moveTask = {
|
||||
// it cannot be updated in the pre update hook
|
||||
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
|
||||
// Only users have a version.
|
||||
if (!group && !challenge) {
|
||||
if (!challenge) {
|
||||
owner._v += 1;
|
||||
}
|
||||
|
||||
@@ -956,9 +926,6 @@ api.addChecklistItem = {
|
||||
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||
await group.updateTask(savedTask, { newCheckListItem });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -989,9 +956,15 @@ api.scoreCheckListItem = {
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id, { userId: user._id });
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
|
||||
if (!task) throw new NotFound(res.t('messageTaskNotFound'));
|
||||
if (!task || (!task.userId && !task.group.id)) throw new NotFound(res.t('messageTaskNotFound'));
|
||||
if (task.userId && task.userId !== user._id) {
|
||||
throw new BadRequest('Cannot score task belonging to another user.');
|
||||
} else if (task.group.id && user.guilds.indexOf(task.group.id) === -1
|
||||
&& user.party._id !== task.group.id) {
|
||||
throw new BadRequest('Cannot score task belonging to another user.');
|
||||
}
|
||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||
|
||||
const item = _.find(task.checklist, { id: req.params.itemId });
|
||||
@@ -1063,9 +1036,6 @@ api.updateChecklistItem = {
|
||||
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||
await group.updateTask(savedTask);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1125,9 +1095,6 @@ api.removeChecklistItem = {
|
||||
const savedTask = await task.save();
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||
await group.updateTask(savedTask, { removedCheckListItemId: req.params.itemId });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import * as Tasks from '../../../models/task';
|
||||
import { model as Group } from '../../../models/group';
|
||||
@@ -12,13 +13,12 @@ import {
|
||||
createTasks,
|
||||
getTasks,
|
||||
groupSubscriptionNotFound,
|
||||
scoreTasks,
|
||||
} from '../../../libs/tasks';
|
||||
import {
|
||||
moveTask,
|
||||
} from '../../../libs/tasks/utils';
|
||||
import { handleSharedCompletion } from '../../../libs/groupTasks';
|
||||
import apiError from '../../../libs/apiError';
|
||||
import logger from '../../../libs/logger';
|
||||
|
||||
const requiredGroupFields = '_id leader tasksOrder name';
|
||||
// @TODO: abstract to task lib
|
||||
@@ -150,14 +150,15 @@ api.groupMoveTask = {
|
||||
|
||||
if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo'));
|
||||
|
||||
const groupFields = requiredGroupFields.concat(' managers purchased');
|
||||
const group = await Group.getGroup({
|
||||
user,
|
||||
groupId: task.group.id,
|
||||
fields: requiredGroupFields.concat(' purchased'),
|
||||
fields: groupFields,
|
||||
});
|
||||
if (groupSubscriptionNotFound(group)) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
const order = group.tasksOrder[`${task.type}s`];
|
||||
|
||||
@@ -184,30 +185,31 @@ api.groupMoveTask = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/tasks/:taskId/assign/:assignedUserId Assign a group task to a user
|
||||
* @apiDescription Assigns a user to a group task
|
||||
* @api {post} /api/v3/tasks/:taskId/assign Assign a group task to a user or users
|
||||
* @apiDescription Assign users to a group task
|
||||
* @apiName AssignTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam (Path) {UUID} taskId The id of the task that will be assigned
|
||||
* @apiParam (Path) {UUID} assignedUserId The id of the user that will be assigned to the task
|
||||
* @apiParam (Body) {UUID[]} [assignedUserIds] Array of user IDs to be assigned to the task
|
||||
*
|
||||
* @apiSuccess data The assigned task
|
||||
*/
|
||||
api.assignTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/assign/:assignedUserId',
|
||||
url: '/tasks/:taskId/assign',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { assignedUserId } = req.params;
|
||||
const assignedUser = await User.findById(assignedUserId).exec();
|
||||
const assignedUserIds = req.body;
|
||||
for (const userId of assignedUserIds) {
|
||||
if (!isUUID(userId)) throw new BadRequest('Assigned users must be UUIDs');
|
||||
}
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
@@ -224,37 +226,36 @@ api.assignTask = {
|
||||
const group = await Group.getGroup({ user, groupId: task.group.id, fields: groupFields });
|
||||
if (groupSubscriptionNotFound(group)) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
const assignedUsers = await User.find({ _id: { $in: assignedUserIds } }).exec();
|
||||
const promises = [];
|
||||
const taskText = task.text;
|
||||
const userName = `@${user.auth.local.username}`;
|
||||
|
||||
if (user._id === assignedUserId) {
|
||||
const managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
const managers = await User.find({ _id: managerIds }, 'notifications preferences').exec();
|
||||
managers.forEach(manager => {
|
||||
if (manager._id === user._id) return;
|
||||
manager.addNotification('GROUP_TASK_CLAIMED', {
|
||||
message: res.t('taskClaimed', { userName, taskText }, manager.preferences.language),
|
||||
for (const userToAssign of assignedUsers) {
|
||||
if (user._id !== userToAssign._id) {
|
||||
userToAssign.addNotification('GROUP_TASK_ASSIGNED', {
|
||||
message: res.t('youHaveBeenAssignedTask', { managerName: userName, taskText }),
|
||||
groupId: group._id,
|
||||
taskId: task._id,
|
||||
});
|
||||
promises.push(manager.save());
|
||||
});
|
||||
} else {
|
||||
assignedUser.addNotification('GROUP_TASK_ASSIGNED', {
|
||||
message: res.t('youHaveBeenAssignedTask', { managerName: userName, taskText }),
|
||||
taskId: task._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
promises.push(group.syncTask(task, assignedUser, user));
|
||||
promises.push(group.syncTask(task, assignedUsers, user));
|
||||
promises.push(group.save());
|
||||
await Promise.all(promises);
|
||||
|
||||
res.respond(200, task);
|
||||
|
||||
res.analytics.track('task assign', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
taskType: task.type,
|
||||
groupID: group._id,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -299,7 +300,7 @@ api.unassignTask = {
|
||||
const group = await Group.getGroup({ user, groupId: task.group.id, fields });
|
||||
if (groupSubscriptionNotFound(group)) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
await group.unlinkTask(task, assignedUser);
|
||||
|
||||
@@ -314,111 +315,6 @@ api.unassignTask = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/tasks/:taskId/approve/:userId Approve a user's task
|
||||
* @apiDescription Approves a user assigned to a group task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName ApproveTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam (Path) {UUID} taskId The id of the task that is the original group task
|
||||
* @apiParam (Path) {UUID} userId The id of the user that will be approved
|
||||
*
|
||||
* @apiSuccess task The approved task
|
||||
*/
|
||||
api.approveTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/approve/:userId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const assignedUserId = req.params.userId;
|
||||
const assignedUser = await User.findById(assignedUserId).exec();
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findOne({
|
||||
'group.taskId': taskId,
|
||||
userId: assignedUserId,
|
||||
}).exec();
|
||||
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('messageTaskNotFound'));
|
||||
}
|
||||
|
||||
const fields = requiredGroupFields.concat(' purchased managers');
|
||||
const group = await Group.getGroup({ user, groupId: task.group.id, fields });
|
||||
if (groupSubscriptionNotFound(group)) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (task.group.approval.approved === true) throw new NotAuthorized(res.t('canOnlyApproveTaskOnce'));
|
||||
if (!task.group.approval.requested) {
|
||||
throw new NotAuthorized(res.t('taskApprovalWasNotRequested'));
|
||||
}
|
||||
|
||||
task.group.approval.dateApproved = new Date();
|
||||
task.group.approval.approvingUser = user._id;
|
||||
task.group.approval.approved = true;
|
||||
|
||||
// Get Managers
|
||||
const managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
const managers = await User.find({ _id: managerIds }, 'notifications').exec(); // Use this method so we can get access to notifications
|
||||
|
||||
// Get task direction
|
||||
const firstManagerNotifications = managers[0].notifications;
|
||||
const firstNotificationIndex = firstManagerNotifications.findIndex(notification => notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL');
|
||||
let direction = 'up';
|
||||
if (firstManagerNotifications[firstNotificationIndex]) {
|
||||
direction = firstManagerNotifications[firstNotificationIndex].direction || direction;
|
||||
}
|
||||
|
||||
// Remove old notifications
|
||||
const approvalPromises = [];
|
||||
managers.forEach(manager => {
|
||||
const notificationIndex = manager.notifications.findIndex(notification => notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL');
|
||||
|
||||
if (notificationIndex !== -1) {
|
||||
manager.notifications.splice(notificationIndex, 1);
|
||||
approvalPromises.push(manager.save());
|
||||
}
|
||||
});
|
||||
|
||||
// Add new notifications to user
|
||||
assignedUser.addNotification('GROUP_TASK_APPROVED', {
|
||||
message: res.t('yourTaskHasBeenApproved', { taskText: task.text }),
|
||||
groupId: group._id,
|
||||
task,
|
||||
direction,
|
||||
});
|
||||
|
||||
approvalPromises.push(task.save());
|
||||
approvalPromises.push(assignedUser.save());
|
||||
await Promise.all(approvalPromises);
|
||||
|
||||
res.respond(200, task);
|
||||
|
||||
// Wrapping everything in a try/catch block because if an error occurs
|
||||
// using `await` it MUST NOT bubble up because the request has already been handled
|
||||
try {
|
||||
const groupTask = await Tasks.Task.findOne({
|
||||
_id: task.group.taskId,
|
||||
}).exec();
|
||||
|
||||
if (groupTask) {
|
||||
await handleSharedCompletion(groupTask, task);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error handling group task', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/tasks/:taskId/needs-work/:userId Require more work for a group task
|
||||
* @apiDescription Mark an assigned group task as needing more work before it can be approved
|
||||
@@ -450,122 +346,54 @@ api.taskNeedsWork = {
|
||||
const [assignedUser, task] = await Promise.all([
|
||||
User.findById(assignedUserId).exec(),
|
||||
await Tasks.Task.findOne({
|
||||
'group.taskId': taskId,
|
||||
userId: assignedUserId,
|
||||
_id: taskId,
|
||||
}).exec(),
|
||||
]);
|
||||
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('messageTaskNotFound'));
|
||||
}
|
||||
if (['daily', 'todo'].indexOf(task.type) === -1) {
|
||||
throw new BadRequest('Cannot roll back use of Habits or Rewards.');
|
||||
}
|
||||
|
||||
if (task.group.completedBy.userId) {
|
||||
if (task.group.completedBy.userId !== assignedUserId) {
|
||||
throw new BadRequest('Task not completed by this user.');
|
||||
}
|
||||
} else if (!task.group.assignedUsersDetail || !task.group.assignedUsersDetail[assignedUserId]
|
||||
|| !task.group.assignedUsersDetail[assignedUserId].completed) {
|
||||
throw new BadRequest('Task not completed by this user.');
|
||||
}
|
||||
|
||||
const fields = requiredGroupFields.concat(' purchased managers');
|
||||
const group = await Group.getGroup({ user, groupId: task.group.id, fields });
|
||||
if (groupSubscriptionNotFound(group)) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (task.group.approval.approved === true) throw new NotAuthorized(res.t('canOnlyApproveTaskOnce'));
|
||||
if (!task.group.approval.requested) {
|
||||
throw new NotAuthorized(res.t('taskApprovalWasNotRequested'));
|
||||
|
||||
await scoreTasks(assignedUser, [{ id: task._id, direction: 'down' }], req, res);
|
||||
if (assignedUserId !== user._id) {
|
||||
assignedUser.addNotification('GROUP_TASK_NEEDS_WORK', {
|
||||
message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.auth.local.username }, assignedUser.preferences.language),
|
||||
task: {
|
||||
id: task._id,
|
||||
text: task.text,
|
||||
},
|
||||
group: {
|
||||
id: group._id,
|
||||
name: group.name,
|
||||
},
|
||||
manager: {
|
||||
id: user._id,
|
||||
name: user.auth.local.username,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Get Managers
|
||||
const managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
const managers = await User.find({ _id: managerIds }, 'notifications').exec(); // Use this method so we can get access to notifications
|
||||
|
||||
const promises = [];
|
||||
|
||||
// Remove old notifications
|
||||
managers.forEach(manager => {
|
||||
const notificationIndex = manager.notifications.findIndex(notification => notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL');
|
||||
|
||||
if (notificationIndex !== -1) {
|
||||
manager.notifications.splice(notificationIndex, 1);
|
||||
promises.push(manager.save());
|
||||
}
|
||||
});
|
||||
|
||||
task.group.approval.requested = false;
|
||||
task.group.approval.requestedDate = undefined;
|
||||
|
||||
const taskText = task.text;
|
||||
const managerName = user.profile.name;
|
||||
|
||||
const message = res.t('taskNeedsWork', { taskText, managerName }, assignedUser.preferences.language);
|
||||
|
||||
assignedUser.addNotification('GROUP_TASK_NEEDS_WORK', {
|
||||
message,
|
||||
task: {
|
||||
id: task._id,
|
||||
text: taskText,
|
||||
},
|
||||
group: {
|
||||
id: group._id,
|
||||
name: group.name,
|
||||
},
|
||||
manager: {
|
||||
id: user._id,
|
||||
name: managerName,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([...promises, assignedUser.save(), task.save()]);
|
||||
await Promise.all([assignedUser.save(), task.save()]);
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/approvals/group/:groupId Get a group's approvals
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetGroupApprovals
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam (Path) {UUID} groupId The id of the group from which to retrieve the approvals
|
||||
*
|
||||
* @apiSuccess {Array} data An array of tasks
|
||||
*/
|
||||
api.getGroupApprovals = {
|
||||
method: 'GET',
|
||||
url: '/approvals/group/:groupId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { groupId } = req.params;
|
||||
|
||||
const fields = requiredGroupFields.concat(' purchased managers');
|
||||
const group = await Group.getGroup({ user, groupId, fields });
|
||||
if (groupSubscriptionNotFound(group)) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let approvals;
|
||||
if (canNotEditTasks(group, user)) {
|
||||
approvals = await Tasks.Task.find({
|
||||
'group.id': groupId,
|
||||
'group.approval.approved': false,
|
||||
'group.approval.requested': true,
|
||||
'group.assignedUsers': user._id,
|
||||
userId: user._id,
|
||||
}, 'userId group text')
|
||||
.populate('userId', 'profile')
|
||||
.exec();
|
||||
} else {
|
||||
approvals = await Tasks.Task.find({
|
||||
'group.id': groupId,
|
||||
'group.approval.approved': false,
|
||||
'group.approval.requested': true,
|
||||
}, 'userId group text')
|
||||
.populate('userId', 'profile')
|
||||
.exec();
|
||||
}
|
||||
|
||||
res.respond(200, approvals);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -343,17 +343,18 @@ export async function cron (options = {}) {
|
||||
if (!user.party.quest.progress.down) user.party.quest.progress.down = 0;
|
||||
|
||||
tasksByType.dailys.forEach(task => {
|
||||
const isTeamBoardTask = task.group.id && !task.userId;
|
||||
if (
|
||||
task.group.assignedDate
|
||||
!isTeamBoardTask && task.group.assignedDate
|
||||
&& moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)
|
||||
) return;
|
||||
const { completed } = task;
|
||||
// Deduct points for missed Daily tasks
|
||||
let EvadeTask = 0;
|
||||
let evadeTask = 0;
|
||||
let scheduleMisses = daysMissed;
|
||||
|
||||
if (completed) {
|
||||
dailyChecked += 1;
|
||||
if (!isTeamBoardTask) dailyChecked += 1;
|
||||
if (!atLeastOneDailyDue) { // only bother checking until the first thing is found
|
||||
const thatDay = moment(now).subtract({ days: daysMissed });
|
||||
atLeastOneDailyDue = shouldDo(thatDay.toDate(), task, user.preferences);
|
||||
@@ -368,15 +369,15 @@ export async function cron (options = {}) {
|
||||
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
|
||||
atLeastOneDailyDue = true;
|
||||
scheduleMisses += 1;
|
||||
if (user.stats.buffs.stealth) {
|
||||
if (user.stats.buffs.stealth && !isTeamBoardTask) {
|
||||
user.stats.buffs.stealth -= 1;
|
||||
EvadeTask += 1;
|
||||
evadeTask += 1;
|
||||
}
|
||||
}
|
||||
if (multiDaysCountAsOneDay) break;
|
||||
}
|
||||
|
||||
if (scheduleMisses > EvadeTask) {
|
||||
if (scheduleMisses > evadeTask) {
|
||||
// The user did not complete this due Daily
|
||||
// (but no penalty if cron is running in safe mode).
|
||||
if (CRON_SAFE_MODE) {
|
||||
@@ -402,7 +403,7 @@ export async function cron (options = {}) {
|
||||
user,
|
||||
task,
|
||||
direction: 'down',
|
||||
times: multiDaysCountAsOneDay ? 1 : scheduleMisses - EvadeTask,
|
||||
times: multiDaysCountAsOneDay ? 1 : scheduleMisses - evadeTask,
|
||||
cron: true,
|
||||
});
|
||||
|
||||
@@ -437,13 +438,6 @@ export async function cron (options = {}) {
|
||||
task.checklist.forEach(i => { i.completed = false; });
|
||||
}
|
||||
}
|
||||
|
||||
if (task.group && task.group.approval && task.group.approval.approved) {
|
||||
task.group.approval.approved = false;
|
||||
task.group.approval.dateApproved = null;
|
||||
task.group.approval.requested = false;
|
||||
task.group.approval.requestedDate = null;
|
||||
}
|
||||
});
|
||||
|
||||
resetHabitCounters(user, tasksByType, now, daysMissed);
|
||||
@@ -454,12 +448,6 @@ export async function cron (options = {}) {
|
||||
if (task.up === false || task.down === false) {
|
||||
task.value = Math.abs(task.value) < 0.1 ? 0 : task.value /= 2;
|
||||
}
|
||||
if (task.group && task.group.approval && task.group.approval.approved) {
|
||||
task.group.approval.approved = false;
|
||||
task.group.approval.dateApproved = null;
|
||||
task.group.approval.requested = false;
|
||||
task.group.approval.requestedDate = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Finished tallying
|
||||
|
||||
@@ -1,56 +1,18 @@
|
||||
import * as Tasks from '../models/task'; // eslint-disable-line import/no-cycle
|
||||
|
||||
const SHARED_COMPLETION = {
|
||||
default: 'recurringCompletion',
|
||||
single: 'singleCompletion',
|
||||
every: 'allAssignedCompletion',
|
||||
};
|
||||
|
||||
async function _completeMasterTask (masterTask) {
|
||||
masterTask.completed = true;
|
||||
await masterTask.save();
|
||||
}
|
||||
|
||||
async function _deleteUnfinishedTasks (groupMemberTask) {
|
||||
await Tasks.Task.deleteMany({
|
||||
'group.taskId': groupMemberTask.group.taskId,
|
||||
$and: [
|
||||
{ userId: { $exists: true } },
|
||||
{ userId: { $ne: groupMemberTask.userId } },
|
||||
],
|
||||
}).exec();
|
||||
}
|
||||
|
||||
async function _evaluateAllAssignedCompletion (masterTask) {
|
||||
let completions;
|
||||
if (masterTask.group.approval && masterTask.group.approval.required) {
|
||||
completions = await Tasks.Task.countDocuments({
|
||||
'group.taskId': masterTask._id,
|
||||
'group.approval.approved': true,
|
||||
}).exec();
|
||||
} else {
|
||||
completions = await Tasks.Task.countDocuments({
|
||||
'group.taskId': masterTask._id,
|
||||
completed: true,
|
||||
}).exec();
|
||||
}
|
||||
if (completions >= masterTask.group.assignedUsers.length) {
|
||||
await _completeMasterTask(masterTask);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSharedCompletion (masterTask, groupMemberTask) {
|
||||
if (masterTask.type !== 'todo') return;
|
||||
|
||||
if (masterTask.group.sharedCompletion === SHARED_COMPLETION.single) {
|
||||
await _deleteUnfinishedTasks(groupMemberTask);
|
||||
await _completeMasterTask(masterTask);
|
||||
} else if (masterTask.group.sharedCompletion === SHARED_COMPLETION.every) {
|
||||
await _evaluateAllAssignedCompletion(masterTask);
|
||||
async function handleSharedCompletion (teamTask) {
|
||||
if (teamTask.type === 'reward') return;
|
||||
const incompleteTask = await Tasks.Task.findOne({
|
||||
'group.taskId': teamTask._id,
|
||||
userId: { $exists: true },
|
||||
completed: false,
|
||||
}, { _id: 1 }).exec();
|
||||
if (!incompleteTask) {
|
||||
teamTask.completed = true;
|
||||
teamTask.save();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
SHARED_COMPLETION,
|
||||
handleSharedCompletion,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
|
||||
import * as Tasks from '../../models/task'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
@@ -220,6 +221,8 @@ async function cancelGroupSubscriptionForUser (user, group, userWasRemoved = fal
|
||||
const index = userGroups.indexOf(group._id);
|
||||
if (index >= 0) userGroups.splice(index, 1);
|
||||
|
||||
await Tasks.Task.remove({ userId: user._id, 'group.id': group._id }).exec();
|
||||
|
||||
const groupPlansQuery = {
|
||||
// type: { $in: ['guild', 'party'] },
|
||||
// privacy: 'private',
|
||||
|
||||
@@ -21,8 +21,8 @@ async function castTaskSpell (res, req, targetId, user, spell, quantity = 1) {
|
||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||
|
||||
const task = await Tasks.Task.findOne({
|
||||
_id: targetId,
|
||||
userId: user._id,
|
||||
_id: targetId,
|
||||
}).exec();
|
||||
if (!task) throw new NotFound(res.t('messageTaskNotFound'));
|
||||
if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast'));
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import compact from 'lodash/compact';
|
||||
import forEach from 'lodash/forEach';
|
||||
import keys from 'lodash/keys';
|
||||
import remove from 'lodash/remove';
|
||||
import validator from 'validator';
|
||||
import {
|
||||
setNextDue,
|
||||
@@ -17,7 +21,6 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../errors';
|
||||
import {
|
||||
SHARED_COMPLETION,
|
||||
handleSharedCompletion,
|
||||
} from '../groupTasks';
|
||||
import shared from '../../../common';
|
||||
@@ -64,10 +67,7 @@ async function createTasks (req, res, options = {}) {
|
||||
newTask.challenge.id = challenge.id;
|
||||
} else if (group) {
|
||||
newTask.group.id = group._id;
|
||||
if (taskData.requiresApproval) {
|
||||
newTask.group.approval.required = true;
|
||||
}
|
||||
newTask.group.sharedCompletion = taskData.sharedCompletion || SHARED_COMPLETION.default;
|
||||
newTask.tags = [group._id];
|
||||
newTask.group.managerNotes = taskData.managerNotes || '';
|
||||
} else {
|
||||
newTask.userId = user._id;
|
||||
@@ -152,23 +152,60 @@ async function getTasks (req, res, options = {}) {
|
||||
dueDate,
|
||||
} = options;
|
||||
|
||||
let query = { userId: user._id };
|
||||
let query;
|
||||
let limit;
|
||||
let sort;
|
||||
let upgradedGroups = [];
|
||||
const upgradedGroupIds = [];
|
||||
const owner = group || challenge || user;
|
||||
|
||||
if (challenge) {
|
||||
query = { 'challenge.id': challenge.id, userId: { $exists: false } };
|
||||
} else if (group) {
|
||||
query = { 'group.id': group._id, userId: { $exists: false } };
|
||||
query = { 'group.id': group._id };
|
||||
} else {
|
||||
const groupsToMirror = user.preferences.tasks.mirrorGroupTasks;
|
||||
if (groupsToMirror && groupsToMirror.length > 0) {
|
||||
upgradedGroups = await Group.find(
|
||||
{
|
||||
_id: { $in: groupsToMirror },
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
},
|
||||
{ _id: 1 },
|
||||
).exec();
|
||||
}
|
||||
if (upgradedGroups.length > 0) {
|
||||
for (const upgradedGroup of upgradedGroups) {
|
||||
upgradedGroupIds.push(upgradedGroup._id);
|
||||
}
|
||||
query = {
|
||||
$or: [
|
||||
{ userId: user._id },
|
||||
{
|
||||
'group.id': { $in: upgradedGroupIds },
|
||||
$or: [
|
||||
{ 'group.assignedUsers': user._id },
|
||||
{ 'group.assignedUsers.0': { $exists: false } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
query = { userId: user._id };
|
||||
}
|
||||
}
|
||||
|
||||
const { type } = req.query;
|
||||
|
||||
if (type) {
|
||||
if (type === 'todos') {
|
||||
query.completed = false; // Exclude completed todos
|
||||
query.type = 'todo';
|
||||
query.completed = false; // Exclude completed todos
|
||||
} else if (type === 'completedTodos' || type === '_allCompletedTodos') { // _allCompletedTodos is currently in BETA and is likely to be removed in future
|
||||
limit = 30;
|
||||
|
||||
@@ -179,7 +216,18 @@ async function getTasks (req, res, options = {}) {
|
||||
query.type = 'todo';
|
||||
query.completed = true;
|
||||
|
||||
if (owner._id === user._id) {
|
||||
if (upgradedGroups.length > 0) {
|
||||
query.$or = [
|
||||
{ userId: user._id },
|
||||
{
|
||||
'group.id': { $in: upgradedGroupIds },
|
||||
$or: [
|
||||
{ 'group.assignedUsers': user._id },
|
||||
{ 'group.completedBy.userId': user._id },
|
||||
],
|
||||
},
|
||||
];
|
||||
} else if (owner._id === user._id) {
|
||||
query.userId = user._id;
|
||||
}
|
||||
|
||||
@@ -190,10 +238,12 @@ async function getTasks (req, res, options = {}) {
|
||||
query.type = type.slice(0, -1); // removing the final "s"
|
||||
}
|
||||
} else {
|
||||
query.$or = [ // Exclude completed todos
|
||||
{ type: 'todo', completed: false },
|
||||
{ type: { $in: ['habit', 'daily', 'reward'] } },
|
||||
];
|
||||
query.$and = [{
|
||||
$or: [ // Exclude completed todos
|
||||
{ type: 'todo', completed: false },
|
||||
{ type: { $in: ['habit', 'daily', 'reward'] } },
|
||||
],
|
||||
}];
|
||||
}
|
||||
|
||||
const mQuery = Tasks.Task.find(query);
|
||||
@@ -208,6 +258,19 @@ async function getTasks (req, res, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
let ownerDirty = false;
|
||||
// Prune nonexistent tasks from tasksOrder
|
||||
forEach(owner.tasksOrder, (taskOrder, key) => {
|
||||
if (type && key.slice(0, -1) !== type) return;
|
||||
const preLength = taskOrder.length;
|
||||
remove(taskOrder, taskId => tasks.findIndex(task => task._id === taskId) === -1);
|
||||
if (preLength !== taskOrder.length) {
|
||||
owner.tasksOrder[key] = taskOrder;
|
||||
owner.markModified('tasksOrder');
|
||||
ownerDirty = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Order tasks based on tasksOrder
|
||||
let order = [];
|
||||
if (type && type !== 'completedTodos' && type !== '_allCompletedTodos') {
|
||||
@@ -228,13 +291,18 @@ async function getTasks (req, res, options = {}) {
|
||||
const i = order[index] === taskId ? index : order.indexOf(taskId);
|
||||
if (i === -1) {
|
||||
unorderedTasks.push(task);
|
||||
const typeString = `${task.type}s`;
|
||||
owner.tasksOrder[typeString].push(taskId);
|
||||
ownerDirty = true;
|
||||
} else {
|
||||
orderedTasks[i] = task;
|
||||
}
|
||||
});
|
||||
|
||||
if (ownerDirty) await owner.save();
|
||||
|
||||
// Remove empty values from the array and add any unordered task
|
||||
orderedTasks = _.compact(orderedTasks).concat(unorderedTasks);
|
||||
orderedTasks = compact(orderedTasks).concat(unorderedTasks);
|
||||
return orderedTasks;
|
||||
}
|
||||
|
||||
@@ -301,22 +369,23 @@ async function handleChallengeTask (task, delta, direction) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGroupTask (task, delta, direction) {
|
||||
async function handleTeamTask (task, delta, direction) {
|
||||
if (task.group && task.group.taskId) {
|
||||
// Wrapping everything in a try/catch block because if an error occurs
|
||||
// using `await` it MUST NOT bubble up because the request has already been handled
|
||||
try {
|
||||
const groupTask = await Tasks.Task.findOne({
|
||||
const teamTask = await Tasks.Task.findOne({
|
||||
_id: task.group.taskId,
|
||||
}).exec();
|
||||
|
||||
if (groupTask) {
|
||||
await handleSharedCompletion(groupTask, task);
|
||||
|
||||
const groupDelta = groupTask.group.assignedUsers
|
||||
? delta / groupTask.group.assignedUsers.length
|
||||
if (teamTask) {
|
||||
const groupDelta = teamTask.group.assignedUsers
|
||||
? delta / keys(teamTask.group.assignedUsers).length
|
||||
: delta;
|
||||
await groupTask.scoreChallengeTask(groupDelta, direction);
|
||||
await teamTask.scoreChallengeTask(groupDelta, direction);
|
||||
if (task.type === 'daily' || task.type === 'todo') {
|
||||
await handleSharedCompletion(teamTask);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e, 'Error scoring group task');
|
||||
@@ -334,106 +403,96 @@ async function handleGroupTask (task, delta, direction) {
|
||||
*/
|
||||
async function scoreTask (user, task, direction, req, res) {
|
||||
if (task.type === 'daily' || task.type === 'todo') {
|
||||
if (task.completed && direction === 'up') {
|
||||
if (task.group.id && task.group.assignedUsersDetail
|
||||
&& task.group.assignedUsersDetail[user._id]
|
||||
) {
|
||||
if (task.group.assignedUsersDetail[user._id].completed && direction === 'up') {
|
||||
throw new NotAuthorized(res.t('sessionOutdated'));
|
||||
} else if (!task.group.assignedUsersDetail[user._id].completed && direction === 'down') {
|
||||
throw new NotAuthorized(res.t('sessionOutdated'));
|
||||
}
|
||||
} else if (task.completed && direction === 'up') {
|
||||
throw new NotAuthorized(res.t('sessionOutdated'));
|
||||
} else if (!task.completed && direction === 'down') {
|
||||
throw new NotAuthorized(res.t('sessionOutdated'));
|
||||
}
|
||||
}
|
||||
|
||||
if (task.group.approval.required && !task.group.approval.approved) {
|
||||
const fields = requiredGroupFields.concat(' managers');
|
||||
const group = await Group.getGroup({ user, groupId: task.group.id, fields });
|
||||
let rollbackUser;
|
||||
let group;
|
||||
|
||||
const managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
|
||||
if (managerIds.indexOf(user._id) !== -1) {
|
||||
task.group.approval.approved = true;
|
||||
task.group.approval.requested = true;
|
||||
task.group.approval.requestedDate = new Date();
|
||||
} else {
|
||||
if (task.group.approval.requested) {
|
||||
return {
|
||||
task,
|
||||
requiresApproval: true,
|
||||
message: res.t('taskRequiresApproval'),
|
||||
};
|
||||
}
|
||||
|
||||
task.group.approval.requested = true;
|
||||
task.group.approval.requestedDate = new Date();
|
||||
|
||||
const managers = await User.find({ _id: managerIds }, 'notifications preferences').exec(); // Use this method so we can get access to notifications
|
||||
|
||||
// @TODO: we can use the User.pushNotification function because
|
||||
// we need to ensure notifications are translated
|
||||
const managerPromises = [];
|
||||
managers.forEach(manager => {
|
||||
manager.addNotification('GROUP_TASK_APPROVAL', {
|
||||
message: res.t('userHasRequestedTaskApproval', {
|
||||
user: user.profile.name,
|
||||
taskName: task.text,
|
||||
}, manager.preferences.language),
|
||||
groupId: group._id,
|
||||
// user task id, used to match the notification when the task is approved
|
||||
taskId: task._id,
|
||||
userId: user._id,
|
||||
groupTaskId: task.group.taskId, // the original task id
|
||||
direction,
|
||||
});
|
||||
managerPromises.push(manager.save());
|
||||
});
|
||||
|
||||
managerPromises.push(task.save());
|
||||
await Promise.all(managerPromises);
|
||||
|
||||
return {
|
||||
task,
|
||||
requiresApproval: true,
|
||||
message: res.t('taskApprovalHasBeenRequested'),
|
||||
};
|
||||
}
|
||||
if (task.group.id) {
|
||||
group = await Group.getGroup({
|
||||
user,
|
||||
groupId: task.group.id,
|
||||
fields: 'leader managers',
|
||||
});
|
||||
}
|
||||
|
||||
if (task.group.approval.required && task.group.approval.approved) {
|
||||
const notificationIndex = user.notifications.findIndex(notification => notification
|
||||
&& notification.data && notification.data.task
|
||||
&& notification.data.task._id === task._id && notification.type === 'GROUP_TASK_APPROVED');
|
||||
|
||||
if (notificationIndex !== -1) {
|
||||
user.notifications.splice(notificationIndex, 1);
|
||||
if (
|
||||
group && task.group.id && !task.userId // Task is on team board
|
||||
&& ['todo', 'daily'].includes(task.type) // Task is a To Do or Daily
|
||||
&& direction === 'down' // Task is being "unchecked"
|
||||
) {
|
||||
const userIsManagement = group.leader === user._id || Boolean(group.managers[user._id]);
|
||||
if (!userIsManagement
|
||||
&& !(task.group.completedBy && task.group.completedBy.userId === user._id)
|
||||
&& !(task.group.assignedUsersDetail && task.group.assignedUsersDetail[user._id])
|
||||
) {
|
||||
throw new BadRequest('Cannot uncheck task you did not complete if not a manager.');
|
||||
}
|
||||
if (task.group.assignedUsers && keys(task.group.assignedUsers).length === 1) {
|
||||
const rollbackUserId = keys(task.group.assignedUsers)[0];
|
||||
rollbackUser = await User.findOne({ _id: rollbackUserId });
|
||||
} else {
|
||||
rollbackUser = await User.findOne({ _id: task.group.completedBy.userId });
|
||||
}
|
||||
task.group.completedBy = {};
|
||||
}
|
||||
|
||||
const wasCompleted = task.completed;
|
||||
|
||||
const firstTask = !user.achievements.completedTask;
|
||||
const delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics);
|
||||
let delta;
|
||||
|
||||
if (rollbackUser) {
|
||||
delta = shared.ops.scoreTask({
|
||||
task,
|
||||
user: rollbackUser,
|
||||
direction,
|
||||
}, req, res.analytics);
|
||||
await rollbackUser.save();
|
||||
} else {
|
||||
delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics);
|
||||
}
|
||||
// Drop system (don't run on the client,
|
||||
// as it would only be discarded since ops are sent to the API, not the results)
|
||||
if (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task, delta }, req, res.analytics);
|
||||
|
||||
// If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list
|
||||
// TODO move to common code?
|
||||
let pullTask = false;
|
||||
let pushTask = false;
|
||||
let pullTask;
|
||||
let pushTask;
|
||||
if (task.type === 'todo') {
|
||||
if (!wasCompleted && task.completed) {
|
||||
// @TODO: mongoose's push and pull should be atomic and help with
|
||||
// our concurrency issues. If not, we need to use this update $pull and $push
|
||||
pullTask = true;
|
||||
// user.tasksOrder.todos.pull(task._id);
|
||||
pullTask = task._id;
|
||||
} else if (
|
||||
wasCompleted
|
||||
&& !task.completed
|
||||
&& user.tasksOrder.todos.indexOf(task._id) === -1
|
||||
) {
|
||||
pushTask = true;
|
||||
// user.tasksOrder.todos.push(task._id);
|
||||
pushTask = task._id;
|
||||
}
|
||||
}
|
||||
|
||||
if (task.completed && task.group.id
|
||||
&& !task.userId && !task.group.assignedUsers) {
|
||||
task.group.completedBy = {
|
||||
userId: user._id,
|
||||
date: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
setNextDue(task, user);
|
||||
|
||||
taskScoredWebhook.send(user, {
|
||||
@@ -443,6 +502,27 @@ async function scoreTask (user, task, direction, req, res) {
|
||||
user,
|
||||
});
|
||||
|
||||
if (group) {
|
||||
let role;
|
||||
if (group.leader === user._id) {
|
||||
role = 'leader';
|
||||
} else if (group.managers[user._id]) {
|
||||
role = 'manager';
|
||||
} else {
|
||||
role = 'member';
|
||||
}
|
||||
res.analytics.track('team task scored', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
taskType: task.type,
|
||||
direction,
|
||||
headers: req.headers,
|
||||
groupID: group._id,
|
||||
role,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
task,
|
||||
delta,
|
||||
@@ -451,7 +531,7 @@ async function scoreTask (user, task, direction, req, res) {
|
||||
pushTask,
|
||||
// clone user._tmp so that it's not overwritten by other score operations
|
||||
// when using the bulk scoring API
|
||||
_tmp: _.cloneDeep(user._tmp),
|
||||
_tmp: cloneDeep(user._tmp),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -519,8 +599,8 @@ export async function scoreTasks (user, taskScorings, req, res) {
|
||||
const pushIDs = [];
|
||||
|
||||
returnDatas.forEach(returnData => {
|
||||
if (returnData.pushTask === true) pushIDs.push(returnData.task._id);
|
||||
if (returnData.pullTask === true) pullIDs.push(returnData.task._id);
|
||||
if (returnData.pushTask) pushIDs.push(returnData.pushTask);
|
||||
if (returnData.pullTask) pullIDs.push(returnData.pullTask);
|
||||
});
|
||||
|
||||
const moveUpdateObject = {};
|
||||
@@ -535,14 +615,7 @@ export async function scoreTasks (user, taskScorings, req, res) {
|
||||
return returnDatas.map(data => {
|
||||
// Handle challenge and group tasks tasks here because the task must have been saved first
|
||||
handleChallengeTask(data.task, data.delta, data.direction);
|
||||
handleGroupTask(data.task, data.delta, data.direction);
|
||||
|
||||
// Handle group tasks that require approval
|
||||
if (data.requiresApproval === true) {
|
||||
return {
|
||||
id: data.task._id, message: data.message, requiresApproval: true,
|
||||
};
|
||||
}
|
||||
handleTeamTask(data.task, data.delta, data.direction);
|
||||
|
||||
return { id: data.task._id, delta: data.delta, _tmp: data._tmp };
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import common from '../../../common';
|
||||
import * as Tasks from '../../models/task';
|
||||
import { model as Groups } from '../../models/group';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
@@ -132,6 +133,34 @@ export async function update (req, res, { isV3 = false }) {
|
||||
await checkNewInputForProfanity(user, res, newBlurb);
|
||||
}
|
||||
|
||||
if (req.body['preferences.tasks.mirrorGroupTasks'] !== undefined) {
|
||||
const groupsToMirror = req.body['preferences.tasks.mirrorGroupTasks'];
|
||||
if (!Array.isArray(groupsToMirror)) {
|
||||
throw new BadRequest('Groups to copy tasks from must be an array.');
|
||||
}
|
||||
const memberGroups = _.clone(user.guilds);
|
||||
if (user.party._id) memberGroups.push(user.party._id);
|
||||
for (const targetGroup of groupsToMirror) {
|
||||
if (memberGroups.indexOf(targetGroup) === -1) {
|
||||
throw new BadRequest(`User not a member of group ${targetGroup}.`);
|
||||
}
|
||||
}
|
||||
|
||||
const matchingGroupsCount = await Groups.countDocuments({
|
||||
_id: { $in: groupsToMirror },
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
if (matchingGroupsCount !== groupsToMirror.length) {
|
||||
throw new BadRequest('Groups to copy tasks from must have subscriptions.');
|
||||
}
|
||||
}
|
||||
|
||||
_.each(req.body, (val, key) => {
|
||||
const purchasable = requiresPurchase[key];
|
||||
|
||||
@@ -140,7 +169,7 @@ export async function update (req, res, { isV3 = false }) {
|
||||
}
|
||||
|
||||
if (key === 'tags') {
|
||||
if (!Array.isArray(val)) throw new BadRequest('mustBeArray');
|
||||
if (!Array.isArray(val)) throw new BadRequest('Tag list must be an array.');
|
||||
|
||||
const removedTagsIds = [];
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ async function cronAsync (req, res) {
|
||||
userId: user._id,
|
||||
$or: [ // Exclude completed todos
|
||||
{ type: 'todo', completed: false },
|
||||
{ type: { $in: ['habit', 'daily', 'reward'] } },
|
||||
{ type: { $in: ['habit', 'daily'] } },
|
||||
],
|
||||
}).exec();
|
||||
|
||||
@@ -117,19 +117,8 @@ async function cronAsync (req, res) {
|
||||
|
||||
// Save user and tasks
|
||||
const toSave = [user.save()];
|
||||
tasks.forEach(async task => {
|
||||
tasks.forEach(task => {
|
||||
if (task.isModified()) toSave.push(task.save());
|
||||
if (task.isModified() && task.group && task.group.taskId) {
|
||||
const groupTask = await Tasks.Task.findOne({
|
||||
_id: task.group.taskId,
|
||||
}).exec();
|
||||
|
||||
if (groupTask) {
|
||||
let delta = (0.9747 ** task.value) * -1;
|
||||
if (groupTask.group.assignedUsers) delta /= groupTask.group.assignedUsers.length;
|
||||
await groupTask.scoreChallengeTask(delta, 'down');
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(toSave);
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ import {
|
||||
import baseModel from '../libs/baseModel';
|
||||
import { sendTxn as sendTxnEmail } from '../libs/email'; // eslint-disable-line import/no-cycle
|
||||
import { sendNotification as sendPushNotification } from '../libs/pushNotifications'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
syncableAttrs,
|
||||
} from '../libs/tasks/utils';
|
||||
import {
|
||||
schema as SubscriptionPlanSchema,
|
||||
} from './subscriptionPlan';
|
||||
@@ -144,6 +141,9 @@ export const schema = new Schema({
|
||||
slug: { $type: String },
|
||||
name: { $type: String },
|
||||
}],
|
||||
cron: {
|
||||
lastProcessed: { $type: Date },
|
||||
},
|
||||
}, {
|
||||
strict: true,
|
||||
minimize: false, // So empty objects are returned
|
||||
@@ -1386,10 +1386,13 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
|
||||
const promises = user.isModified() ? [user.save()] : [];
|
||||
|
||||
// remove the group from the user's groups
|
||||
const userUpdate = { $pull: { 'preferences.tasks.mirrorGroupTasks': group._id } };
|
||||
if (group.type === 'guild') {
|
||||
promises.push(User.update({ _id: user._id }, { $pull: { guilds: group._id } }).exec());
|
||||
userUpdate.$pull.guilds = group._id;
|
||||
promises.push(User.update({ _id: user._id }, userUpdate).exec());
|
||||
} else {
|
||||
promises.push(User.update({ _id: user._id }, { $set: { party: {} } }).exec());
|
||||
userUpdate.$set = { party: {} };
|
||||
promises.push(User.update({ _id: user._id }, userUpdate).exec());
|
||||
|
||||
update.$unset = { [`quest.members.${user._id}`]: 1 };
|
||||
}
|
||||
@@ -1441,132 +1444,50 @@ schema.methods.unlinkTags = function unlinkTags (user) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates all linked tasks for a group task
|
||||
*
|
||||
* @param taskToSync The group task that will be synced
|
||||
* @param options.newCheckListItem The new checklist item
|
||||
* that needs to be synced to all assigned users
|
||||
* @param options.removedCheckListItem The removed checklist item that
|
||||
* needs to be removed from all assigned users
|
||||
*
|
||||
* @return The created tasks
|
||||
*/
|
||||
schema.methods.updateTask = async function updateTask (taskToSync, options = {}) {
|
||||
const group = this;
|
||||
|
||||
const updateCmd = { $set: {} };
|
||||
|
||||
const syncableAttributes = syncableAttrs(taskToSync);
|
||||
for (const key of Object.keys(syncableAttributes)) {
|
||||
updateCmd.$set[key] = syncableAttributes[key];
|
||||
}
|
||||
|
||||
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
|
||||
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
|
||||
updateCmd.$set['group.sharedCompletion'] = taskToSync.group.sharedCompletion;
|
||||
updateCmd.$set['group.managerNotes'] = taskToSync.group.managerNotes;
|
||||
|
||||
const taskSchema = Tasks[taskToSync.type];
|
||||
|
||||
const updateQuery = {
|
||||
userId: { $exists: true },
|
||||
'group.id': group.id,
|
||||
'group.taskId': taskToSync._id,
|
||||
};
|
||||
|
||||
if (options.newCheckListItem) {
|
||||
const newCheckList = { completed: false };
|
||||
newCheckList.linkId = options.newCheckListItem.id;
|
||||
newCheckList.text = options.newCheckListItem.text;
|
||||
updateCmd.$push = { checklist: newCheckList };
|
||||
}
|
||||
|
||||
if (options.removedCheckListItemId) {
|
||||
updateCmd.$pull = { checklist: { linkId: { $in: [options.removedCheckListItemId] } } };
|
||||
}
|
||||
|
||||
if (options.updateCheckListItems) {
|
||||
updateCmd.$set.checklist = taskToSync.checklist;
|
||||
}
|
||||
|
||||
// Updating instead of loading and saving for performances,
|
||||
// risks becoming a problem if we introduce more complexity in tasks
|
||||
await taskSchema.update(updateQuery, updateCmd, { multi: true }).exec();
|
||||
};
|
||||
|
||||
schema.methods.syncTask = async function groupSyncTask (taskToSync, user, assigningUser) {
|
||||
schema.methods.syncTask = async function groupSyncTask (taskToSync, users, assigningUser) {
|
||||
const group = this;
|
||||
const toSave = [];
|
||||
for (const user of users) {
|
||||
const assignmentData = {
|
||||
assignedDate: new Date(),
|
||||
assignedUsername: user.auth.local.username,
|
||||
assigningUsername: assigningUser.auth.local.username,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
if (taskToSync.group.assignedUsers.indexOf(user._id) === -1) {
|
||||
taskToSync.group.assignedUsers.push(user._id);
|
||||
}
|
||||
|
||||
// Sync tags
|
||||
const userTags = user.tags;
|
||||
const i = _.findIndex(userTags, { id: group._id });
|
||||
|
||||
if (i !== -1) {
|
||||
if (userTags[i].name !== group.name) {
|
||||
// update the name - it's been changed since
|
||||
userTags[i].name = group.name;
|
||||
userTags[i].group = group._id;
|
||||
if (!taskToSync.group.assignedUsers) {
|
||||
taskToSync.group.assignedUsers = [];
|
||||
}
|
||||
} else {
|
||||
userTags.push({
|
||||
id: group._id,
|
||||
name: group.name,
|
||||
group: group._id,
|
||||
});
|
||||
taskToSync.group.assignedUsers.push(user._id);
|
||||
|
||||
if (!taskToSync.group.assignedUsersDetail) {
|
||||
taskToSync.group.assignedUsersDetail = {};
|
||||
}
|
||||
if (!taskToSync.group.assignedUsersDetail[user._id]) {
|
||||
taskToSync.group.assignedUsersDetail[user._id] = assignmentData;
|
||||
}
|
||||
taskToSync.markModified('group.assignedUsersDetail');
|
||||
|
||||
// Sync tags
|
||||
const userTags = user.tags;
|
||||
const i = _.findIndex(userTags, { id: group._id });
|
||||
|
||||
if (i !== -1) {
|
||||
if (userTags[i].name !== group.name) {
|
||||
// update the name - it's been changed since
|
||||
userTags[i].name = group.name;
|
||||
userTags[i].group = group._id;
|
||||
}
|
||||
} else {
|
||||
userTags.push({
|
||||
id: group._id,
|
||||
name: group.name,
|
||||
group: group._id,
|
||||
});
|
||||
}
|
||||
toSave.push(user.save());
|
||||
}
|
||||
|
||||
const findQuery = {
|
||||
'group.taskId': taskToSync._id,
|
||||
userId: user._id,
|
||||
'group.id': group._id,
|
||||
};
|
||||
|
||||
let matchingTask = await Tasks.Task.findOne(findQuery).exec();
|
||||
|
||||
if (!matchingTask) { // If the task is new, create it
|
||||
matchingTask = new Tasks[taskToSync.type](Tasks.Task.sanitize(syncableAttrs(taskToSync)));
|
||||
matchingTask.group.id = taskToSync.group.id;
|
||||
matchingTask.userId = user._id;
|
||||
matchingTask.group.taskId = taskToSync._id;
|
||||
matchingTask.group.assignedDate = new Date();
|
||||
user.tasksOrder[`${taskToSync.type}s`].unshift(matchingTask._id);
|
||||
} else {
|
||||
_.merge(matchingTask, syncableAttrs(taskToSync));
|
||||
// Make sure the task is in user.tasksOrder
|
||||
const orderList = user.tasksOrder[`${taskToSync.type}s`];
|
||||
if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id);
|
||||
}
|
||||
|
||||
matchingTask.group.approval.required = taskToSync.group.approval.required;
|
||||
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
|
||||
matchingTask.group.sharedCompletion = taskToSync.group.sharedCompletion;
|
||||
matchingTask.group.managerNotes = taskToSync.group.managerNotes;
|
||||
if (assigningUser && user._id !== assigningUser._id) {
|
||||
matchingTask.group.assigningUsername = assigningUser.auth.local.username;
|
||||
}
|
||||
|
||||
// sync checklist
|
||||
if (taskToSync.checklist) {
|
||||
taskToSync.checklist.forEach(element => {
|
||||
const newCheckList = { completed: false };
|
||||
newCheckList.linkId = element.id;
|
||||
newCheckList.text = element.text;
|
||||
matchingTask.checklist.push(newCheckList);
|
||||
});
|
||||
}
|
||||
|
||||
// don't override the notes, but provide it if not provided
|
||||
if (!matchingTask.notes) matchingTask.notes = taskToSync.notes;
|
||||
// add tag if missing
|
||||
if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id);
|
||||
|
||||
toSave.push(matchingTask.save(), taskToSync.save(), user.save());
|
||||
toSave.push(taskToSync.save());
|
||||
return Promise.all(toSave);
|
||||
};
|
||||
|
||||
@@ -1576,11 +1497,15 @@ schema.methods.unlinkTask = async function groupUnlinkTask (
|
||||
) {
|
||||
const findQuery = {
|
||||
'group.taskId': unlinkingTask._id,
|
||||
userId: user._id,
|
||||
'group.assignedUsers': user._id,
|
||||
};
|
||||
|
||||
delete unlinkingTask.group.assignedUsersDetail[user._id];
|
||||
const assignedUserIndex = unlinkingTask.group.assignedUsers.indexOf(user._id);
|
||||
unlinkingTask.group.assignedUsers.splice(assignedUserIndex, 1);
|
||||
unlinkingTask.markModified('group');
|
||||
|
||||
const promises = [unlinkingTask.save()];
|
||||
|
||||
if (keep === 'keep-all') {
|
||||
await Tasks.Task.update(findQuery, {
|
||||
@@ -1598,16 +1523,14 @@ schema.methods.unlinkTask = async function groupUnlinkTask (
|
||||
user.markModified('tasksOrder');
|
||||
}
|
||||
|
||||
const promises = [unlinkingTask.save()];
|
||||
if (task) {
|
||||
promises.push(task.remove());
|
||||
}
|
||||
// When multiple tasks are being unlinked at the same time,
|
||||
// save the user once outside of this function
|
||||
if (saveUser) promises.push(user.save());
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
schema.methods.removeTask = async function groupRemoveTask (task) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import _ from 'lodash';
|
||||
import shared from '../../common';
|
||||
import baseModel from '../libs/baseModel';
|
||||
import { preenHistory } from '../libs/preening';
|
||||
import { SHARED_COMPLETION } from '../libs/groupTasks'; // eslint-disable-line import/no-cycle
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
@@ -128,25 +127,24 @@ export const TaskSchema = new Schema({
|
||||
|
||||
group: {
|
||||
id: { $type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid for group task.'] },
|
||||
broken: { $type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED'] },
|
||||
assignedDate: { $type: Date }, // To be removed
|
||||
assigningUsername: { $type: String }, // To be removed
|
||||
assignedUsers: [{ $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for group assigned user.'] }],
|
||||
assignedDate: { $type: Date },
|
||||
assigningUsername: { $type: String },
|
||||
assignedUsersDetail: {
|
||||
$type: Schema.Types.Mixed,
|
||||
// key is assigned UUID, with
|
||||
// { assignedDate: Date,
|
||||
// assignedUsername: '@username',
|
||||
// assigningUsername: '@username',
|
||||
// completed: Boolean,
|
||||
// completedDate: Date }
|
||||
},
|
||||
taskId: { $type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid for group task.'] },
|
||||
approval: {
|
||||
required: { $type: Boolean, default: false },
|
||||
approved: { $type: Boolean, default: false },
|
||||
dateApproved: { $type: Date },
|
||||
approvingUser: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for group approving user.'] },
|
||||
requested: { $type: Boolean, default: false },
|
||||
requestedDate: { $type: Date },
|
||||
},
|
||||
sharedCompletion: {
|
||||
$type: String,
|
||||
enum: _.values(SHARED_COMPLETION),
|
||||
default: SHARED_COMPLETION.single,
|
||||
},
|
||||
managerNotes: { $type: String },
|
||||
completedBy: {
|
||||
userId: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for task completing user.'] },
|
||||
date: { $type: Date },
|
||||
},
|
||||
},
|
||||
|
||||
reminders: [reminderSchema],
|
||||
@@ -207,7 +205,7 @@ TaskSchema.statics.findByIdOrAlias = async function findByIdOrAlias (
|
||||
return task;
|
||||
};
|
||||
|
||||
TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias (
|
||||
TaskSchema.statics.findMultipleByIdOrAlias = async function findMultipleByIdOrAlias (
|
||||
identifiers,
|
||||
userId,
|
||||
additionalQueries = {},
|
||||
@@ -216,8 +214,6 @@ TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias (
|
||||
if (!userId) throw new Error('User identifier is a required argument');
|
||||
|
||||
const query = _.cloneDeep(additionalQueries);
|
||||
query.userId = userId;
|
||||
|
||||
const ids = [];
|
||||
const aliases = [];
|
||||
|
||||
@@ -229,10 +225,20 @@ TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias (
|
||||
}
|
||||
});
|
||||
|
||||
query.$or = [
|
||||
{ _id: { $in: ids } },
|
||||
{ alias: { $in: aliases } },
|
||||
];
|
||||
if (ids.length > 0 && aliases.length > 0) {
|
||||
query.userId = userId;
|
||||
query.$or = [
|
||||
{ _id: { $in: ids } },
|
||||
{ alias: { $in: aliases } },
|
||||
];
|
||||
} else if (ids.length > 0) {
|
||||
query._id = { $in: ids };
|
||||
} else if (aliases.length > 0) {
|
||||
query.userId = userId;
|
||||
query.alias = { $in: aliases };
|
||||
} else {
|
||||
throw new Error('No identifiers found.'); // Should be covered by the !identifiers check, but..
|
||||
}
|
||||
|
||||
const tasks = await this.find(query).exec();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
defaults, map, flatten, flow, compact, uniq, partialRight,
|
||||
defaults, map, flatten, flow, compact, uniq, partialRight, remove,
|
||||
} from 'lodash';
|
||||
import common from '../../../common';
|
||||
|
||||
@@ -502,6 +502,21 @@ schema.methods.isMemberOfGroupPlan = async function isMemberOfGroupPlan () {
|
||||
return groups.some(g => g.hasActiveGroupPlan());
|
||||
};
|
||||
|
||||
schema.methods.teamsLed = async function teamsLed () {
|
||||
const user = this;
|
||||
const groups = await getUserGroupData(user);
|
||||
|
||||
remove(groups, group => !group.hasActiveGroupPlan);
|
||||
remove(groups, group => user._id !== group.leader);
|
||||
|
||||
const groupIds = [];
|
||||
groups.forEach(group => {
|
||||
groupIds.push(group._id);
|
||||
});
|
||||
|
||||
return groupIds;
|
||||
};
|
||||
|
||||
schema.methods.isAdmin = function isAdmin () {
|
||||
return Boolean(this.contributor && this.contributor.admin);
|
||||
};
|
||||
|
||||
@@ -236,6 +236,7 @@ export default new Schema({
|
||||
mounts: { $type: Number, default: -1 },
|
||||
hall: { $type: Number, default: -1 },
|
||||
equipment: { $type: Number, default: -1 },
|
||||
groupPlans: { $type: Number, default: -1 },
|
||||
},
|
||||
tutorial: {
|
||||
common: {
|
||||
@@ -583,6 +584,9 @@ export default new Schema({
|
||||
tasks: {
|
||||
groupByChallenge: { $type: Boolean, default: false }, // @TODO remove? not used
|
||||
confirmScoreNotes: { $type: Boolean, default: false }, // @TODO remove? not used
|
||||
mirrorGroupTasks: [
|
||||
{ $type: String, validate: [v => validator.isUUID(v), 'Invalid group UUID.'], ref: 'Group' },
|
||||
],
|
||||
},
|
||||
improvementCategories: {
|
||||
$type: Array,
|
||||
|
||||