WIP(multiassign): resume shared completion implementation

This commit is contained in:
SabreCat 2021-11-11 16:32:30 -06:00
parent 1f81e1971b
commit fa99458ca4
7 changed files with 80 additions and 120 deletions

View file

@ -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();

View file

@ -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

View file

@ -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

View file

@ -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);
},
};

View file

@ -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,
};

View file

@ -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 };
});

View file

@ -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.'] },