mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-05-19 12:18:51 +00:00
WIP(multiassign): resume shared completion implementation
This commit is contained in:
parent
1f81e1971b
commit
fa99458ca4
7 changed files with 80 additions and 120 deletions
|
|
@ -454,19 +454,19 @@
|
|||
class="col-10 mb-1"
|
||||
>{{ $t('assignedTo') }}</label>
|
||||
<a
|
||||
v-if="assignedMember"
|
||||
v-if="assignedMembers.length > 0"
|
||||
class="col-2 text-right mt-1"
|
||||
@click="toggleAssignment(assignedMember._id)"
|
||||
>
|
||||
{{ $t('clear') }}
|
||||
</a>
|
||||
<div class="col-12">
|
||||
<select-single
|
||||
:key="assignedMember"
|
||||
<select-multi
|
||||
ref="assignMembers"
|
||||
:all-items="membersNameAndId"
|
||||
:empty-message="$t('unassigned')"
|
||||
:pill-invert="true"
|
||||
:search-placeholder="$t('chooseTeamMember')"
|
||||
:selected-item="assignedMember"
|
||||
:selected-items="assignedMembers"
|
||||
@toggle="toggleAssignment($event)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1056,11 +1056,9 @@ import goldIcon from '@/assets/svg/gold.svg';
|
|||
import chevronIcon from '@/assets/svg/chevron.svg';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
||||
import gripIcon from '@/assets/svg/grip.svg';
|
||||
import SelectSingle from './modal-controls/selectSingle';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SelectSingle,
|
||||
SelectMulti,
|
||||
Datepicker,
|
||||
checklist,
|
||||
|
|
@ -1093,7 +1091,7 @@ export default {
|
|||
members: [],
|
||||
membersNameAndId: [],
|
||||
memberNamesById: {},
|
||||
assignedMember: null,
|
||||
assignedMembers: [],
|
||||
managers: [],
|
||||
showAdvancedOptions: false,
|
||||
attributesStrings: {
|
||||
|
|
@ -1284,10 +1282,9 @@ export default {
|
|||
});
|
||||
this.memberNamesById[member._id] = member.profile.name;
|
||||
});
|
||||
this.assignedMember = null;
|
||||
this.assignedMembers = [];
|
||||
if (this.task.group?.assignedUsers?.length > 0) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.assignedMember = this.task.group.assignedUsers[0];
|
||||
this.assignedMembers = this.task.group.assignedUsers;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1472,13 +1469,12 @@ export default {
|
|||
tasks: [this.task],
|
||||
});
|
||||
Object.assign(this.task, response);
|
||||
if (this.assignedMember) {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: this.assignedMember,
|
||||
});
|
||||
this.task.group.assignedUsers = [this.assignedMember];
|
||||
}
|
||||
const promises = this.assignedMembers.map(memberId => this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: memberId,
|
||||
}));
|
||||
Promise.all(promises);
|
||||
this.task.group.assignedUsers = this.assignedMembers;
|
||||
this.$emit('taskCreated', this.task);
|
||||
} else {
|
||||
this.createTask(this.task);
|
||||
|
|
@ -1505,21 +1501,21 @@ export default {
|
|||
this.$emit('cancel');
|
||||
},
|
||||
async toggleAssignment (memberId) {
|
||||
if (this.purpose === 'edit') {
|
||||
if (this.assignedMember && this.assignedMember !== memberId) {
|
||||
await this.$store.dispatch('tasks:unassignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: this.assignedMember,
|
||||
});
|
||||
}
|
||||
if (memberId) {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: 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,
|
||||
userId: memberId,
|
||||
});
|
||||
} else {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
userId: memberId,
|
||||
});
|
||||
}
|
||||
this.assignedMember = memberId;
|
||||
},
|
||||
focusInput () {
|
||||
this.$refs.inputToFocus.focus();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -234,11 +234,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
|
||||
|
|
|
|||
|
|
@ -775,17 +775,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);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +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 _deleteUnfinishedTasks (groupMemberTask) {
|
||||
await Tasks.Task.deleteMany({
|
||||
'group.taskId': groupMemberTask.group.taskId,
|
||||
$and: [
|
||||
{ userId: { $exists: true } },
|
||||
{ userId: { $ne: groupMemberTask.userId } },
|
||||
],
|
||||
}).exec();
|
||||
}
|
||||
|
||||
async function handleSharedCompletion (masterTask, groupMemberTask) {
|
||||
if (masterTask.type === 'reward') return;
|
||||
if (masterTask.type === 'todo') await _deleteUnfinishedTasks(groupMemberTask);
|
||||
masterTask.completed = groupMemberTask.completed;
|
||||
masterTask.group.completedBy = groupMemberTask.userId;
|
||||
await masterTask.save();
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
NotAuthorized,
|
||||
} from '../errors';
|
||||
import {
|
||||
SHARED_COMPLETION,
|
||||
handleSharedCompletion,
|
||||
} from '../groupTasks';
|
||||
import shared from '../../../common';
|
||||
|
|
@ -67,7 +66,6 @@ async function createTasks (req, res, options = {}) {
|
|||
if (taskData.requiresApproval) {
|
||||
newTask.group.approval.required = true;
|
||||
}
|
||||
newTask.group.sharedCompletion = taskData.sharedCompletion || SHARED_COMPLETION.default;
|
||||
newTask.group.managerNotes = taskData.managerNotes || '';
|
||||
} else {
|
||||
newTask.userId = user._id;
|
||||
|
|
@ -296,22 +294,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 / 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');
|
||||
|
|
@ -371,17 +370,23 @@ async function scoreTask (user, task, direction, req, res) {
|
|||
if (!localTask) throw new NotFound('Task not found.');
|
||||
}
|
||||
|
||||
const wasCompleted = task.completed;
|
||||
const targetTask = localTask || task;
|
||||
|
||||
const wasCompleted = targetTask.completed;
|
||||
const firstTask = !user.achievements.completedTask;
|
||||
let delta;
|
||||
|
||||
if (rollbackUser) {
|
||||
delta = shared.ops.scoreTask({ task, user: rollbackUser, direction }, req, res.analytics);
|
||||
delta = shared.ops.scoreTask({
|
||||
task: targetTask,
|
||||
user: rollbackUser,
|
||||
direction,
|
||||
}, req, res.analytics);
|
||||
rollbackUser.addNotification('GROUP_TASK_NEEDS_WORK', {
|
||||
message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.profile.name }, rollbackUser.preferences.language),
|
||||
message: res.t('taskNeedsWork', { taskText: targetTask.text, managerName: user.profile.name }, rollbackUser.preferences.language),
|
||||
task: {
|
||||
id: task._id,
|
||||
text: task.text,
|
||||
id: targetTask._id,
|
||||
text: targetTask.text,
|
||||
},
|
||||
group: {
|
||||
id: group._id,
|
||||
|
|
@ -394,51 +399,51 @@ async function scoreTask (user, task, direction, req, res) {
|
|||
});
|
||||
await rollbackUser.save();
|
||||
} else {
|
||||
delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics);
|
||||
delta = shared.ops.scoreTask({ task: targetTask, 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 (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task: targetTask, 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;
|
||||
let pushTask;
|
||||
if (task.type === 'todo') {
|
||||
if (targetTask.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 = localTask ? localTask._id : task._id;
|
||||
pullTask = targetTask._id;
|
||||
} else if (
|
||||
wasCompleted
|
||||
&& !task.completed
|
||||
&& !targetTask.completed
|
||||
&& user.tasksOrder.todos.indexOf(task._id) === -1
|
||||
) {
|
||||
pushTask = localTask ? localTask._id : task._id;
|
||||
pushTask = targetTask._id;
|
||||
}
|
||||
}
|
||||
|
||||
if (task.completed && task.group.id && !task.userId) {
|
||||
task.group.completedBy = user._id;
|
||||
if (targetTask.completed && targetTask.group.id && !targetTask.userId) {
|
||||
targetTask.group.completedBy = user._id;
|
||||
}
|
||||
|
||||
setNextDue(task, user);
|
||||
|
||||
if (localTask) {
|
||||
localTask.completed = task.completed;
|
||||
localTask.value = Number(task.value) + Number(delta);
|
||||
localTask.completed = targetTask.completed;
|
||||
localTask.value = Number(targetTask.value) + Number(delta);
|
||||
await localTask.save();
|
||||
}
|
||||
|
||||
taskScoredWebhook.send(user, {
|
||||
task,
|
||||
task: targetTask,
|
||||
direction,
|
||||
delta,
|
||||
user,
|
||||
});
|
||||
|
||||
return {
|
||||
task,
|
||||
task: targetTask,
|
||||
delta,
|
||||
direction,
|
||||
pullTask,
|
||||
|
|
@ -529,7 +534,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);
|
||||
handleTeamTask(data.task, data.delta, data.direction);
|
||||
|
||||
return { id: data.task._id, delta: data.delta, _tmp: data._tmp };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -142,9 +141,7 @@ export const TaskSchema = new Schema({
|
|||
requestedDate: { $type: Date },
|
||||
},
|
||||
sharedCompletion: {
|
||||
$type: String,
|
||||
enum: _.values(SHARED_COMPLETION),
|
||||
default: SHARED_COMPLETION.single,
|
||||
$type: String, // legacy data
|
||||
},
|
||||
managerNotes: { $type: String },
|
||||
completedBy: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for group completing user.'] },
|
||||
|
|
|
|||
Loading…
Reference in a new issue