diff --git a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js
index 346760bd32..0364869af9 100644
--- a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js
+++ b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js
@@ -63,6 +63,38 @@ describe('Groups DELETE /tasks/:id', () => {
});
});
+ 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,
+ });
+ let memberTasks = await member.get('/tasks/user');
+ let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
+ await user.sync();
+ await member2.sync();
+ expect(user.notifications.length).to.equal(2);
+ expect(user.notifications[1].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.del(`/tasks/${task._id}`);
+
+ await user.sync();
+ await member2.sync();
+
+ expect(user.notifications.length).to.equal(1);
+ expect(member2.notifications.length).to.equal(1);
+ });
+
it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`);
diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js
index c4a65c72fa..a483eaa7fa 100644
--- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js
+++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js
@@ -53,18 +53,29 @@ describe('POST /tasks/:id/approve/:userId', () => {
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
- await user.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
+ await user.post(`/tasks/${task._id}/approve/${member._id}`);
+
await member.sync();
- expect(member.notifications.length).to.equal(2);
- expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
- expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
- expect(member.notifications[1].type).to.equal('SCORED_TASK');
+ expect(member.notifications.length).to.equal(3);
+ expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
+ expect(member.notifications[2].type).to.equal('SCORED_TASK');
+ 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);
@@ -77,18 +88,28 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
- await member2.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
+ await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
- expect(member.notifications.length).to.equal(2);
- expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
- expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
- expect(member.notifications[1].type).to.equal('SCORED_TASK');
+ expect(member.notifications.length).to.equal(3);
+ expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
+ expect(member.notifications[2].type).to.equal('SCORED_TASK');
+ 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);
@@ -132,6 +153,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
+
+ let memberTasks = await member.get('/tasks/user');
+ let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
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({
@@ -141,6 +172,17 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
});
+ 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 () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
@@ -151,6 +193,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
+
+ let memberTasks = await member.get('/tasks/user');
+ let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
@@ -172,6 +224,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
+
+ let memberTasks = await member.get('/tasks/user');
+ let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let member2Tasks = await member2.get('/tasks/user');
@@ -193,6 +255,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
+
+ let memberTasks = await member.get('/tasks/user');
+ let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
@@ -214,6 +286,25 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
+
+ let memberTasks = await member.get('/tasks/user');
+ let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
+ let member2Tasks = await member2.get('/tasks/user');
+ let member2SyncedTask = find(member2Tasks, findAssignedTask);
+ await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js
index 0be0beb720..a474e97732 100644
--- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js
+++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js
@@ -51,7 +51,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
});
- it('marks as task as needing more work', async () => {
+ it('marks a task as needing more work', async () => {
const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`);
@@ -77,7 +77,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct
- expect(member.notifications.length).to.equal(initialNotifications + 1);
+ expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -131,7 +131,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
- expect(member.notifications.length).to.equal(initialNotifications + 1);
+ expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -167,6 +167,17 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
+
+ let memberTasks = await member.get('/tasks/user');
+ let syncedTask = find(memberTasks, findAssignedTask);
+
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
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({
diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js
index 41f6c2ae2b..43380b64dc 100644
--- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js
+++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js
@@ -129,6 +129,13 @@ describe('POST /tasks/:id/score/:direction', () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
+ await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
+ .to.eventually.be.rejected.and.to.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('taskApprovalHasBeenRequested'),
+ });
+
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
diff --git a/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js b/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js
index 852f94bc86..77af1aba29 100644
--- a/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js
+++ b/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js
@@ -113,6 +113,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
expect(syncedTask).to.exist;
});
+ it('sends a notification to assigned user', async () => {
+ await user.post(`/tasks/${task._id}/assign/${member._id}`);
+ await member.sync();
+
+ let groupTask = await user.get(`/tasks/group/${guild._id}`);
+
+ expect(member.notifications.length).to.equal(1);
+ expect(member.notifications[0].type).to.equal('GROUP_TASK_ASSIGNED');
+ expect(member.notifications[0].taskId).to.equal(groupTask._id);
+ });
+
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}`);
diff --git a/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js b/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js
index 4c82c2e895..a3fbd3c8bb 100644
--- a/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js
+++ b/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js
@@ -86,6 +86,13 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist;
});
+ it('removes task assignment notification from unassigned user', async () => {
+ await user.post(`/tasks/${task._id}/unassign/${member._id}`);
+
+ await member.sync();
+ expect(member.notifications.length).to.equal(0);
+ });
+
it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
diff --git a/website/client/components/group-plans/taskInformation.vue b/website/client/components/group-plans/taskInformation.vue
index b6312e0764..c1835d3bb0 100644
--- a/website/client/components/group-plans/taskInformation.vue
+++ b/website/client/components/group-plans/taskInformation.vue
@@ -16,54 +16,7 @@
h1 {{ $t('groupTasksTitle') }}
// @TODO: Abstract to component!
.col-12.col-md-4
- .input-group
- input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
- .filter-panel(v-if="isFilterPanelOpen")
- .tags-category(v-for="tagsType in tagsByType", v-if="tagsType.tags.length > 0", :key="tagsType.key")
- .tags-header.col-12
- strong(v-once) {{ $t(tagsType.key) }}
- a.d-block(v-if="tagsType.key === 'tags' && !editingTags", @click="editTags()") {{ $t('editTags2') }}
- .tags-list.container.col-12
- .row(:class="{'no-gutters': !editingTags}")
- template(v-if="editingTags && tagsType.key === 'tags'")
- .col-12.col-md-6(v-for="(tag, tagIndex) in tagsSnap")
- .inline-edit-input-group.tag-edit-item.input-group
- input.tag-edit-input.inline-edit-input.form-control(type="text", :value="tag.name")
- .input-group-append(@click="removeTag(tagIndex)")
- .svg-icon.destroy-icon(v-html="icons.destroy")
- .col-12.col-md-6
- input.new-tag-item.edit-tag-item.inline-edit-input.form-control(type="text", :placeholder="$t('newTag')", @keydown.enter="addTag($event)", v-model="newTag")
- template(v-else)
- .col-12.col-md-6(v-for="(tag, tagIndex) in tagsType.tags")
- .custom-control.custom-checkbox
- input.custom-control-input(
- type="checkbox",
- :checked="isTagSelected(tag)",
- @change="toggleTag(tag)",
- :id="`tag-${tagsType.key}-${tagIndex}`",
- )
- label.custom-control-label(:for="`tag-${tagsType.key}-${tagIndex}`") {{ tag.name }}
-
- .filter-panel-footer.clearfix
- template(v-if="editingTags === true")
- .text-center
- a.mr-3.btn-filters-primary(@click="saveTags()", v-once) {{ $t('saveEdits') }}
- a.btn-filters-secondary(@click="cancelTagsEditing()", v-once) {{ $t('cancel') }}
- template(v-else)
- .float-left
- a.btn-filters-danger(@click="resetFilters()", v-once) {{ $t('resetFilters') }}
- .float-right
- a.mr-3.btn-filters-primary(@click="applyFilters()", v-once) {{ $t('applyFilters') }}
- a.btn-filters-secondary(@click="closeFilterPanel()", v-once) {{ $t('cancel') }}
- span.input-group-append
- button.btn.btn-secondary.filter-button(
- type="button",
- @click="toggleFilterPanel()",
- :class="{'filter-button-open': selectedTags.length > 0}",
- )
- .d-flex.align-items-center
- span(v-once) {{ $t('filter') }}
- .svg-icon.filter-icon(v-html="icons.filter")
+ input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
.create-task-area.d-flex(v-if='canCreateTasks')
transition(name="slide-tasks-btns")
.d-flex(v-if="openCreateBtn")
@@ -99,10 +52,6 @@
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/create-task.scss';
- .user-tasks-page {
- padding-top: 31px;
- }
-
.tasks-navigation {
margin-bottom: 40px;
}
@@ -114,133 +63,6 @@
margin-right: 8px;
padding-top: 6px;
}
-
- .dropdown-icon-item .svg-icon {
- width: 16px;
- }
-
- button.btn.btn-secondary.filter-button {
- box-shadow: none;
- border-radius: 2px;
- border: 1px solid $gray-400 !important;
-
- &:hover, &:active, &:focus, &.open {
- box-shadow: none;
- border-color: $purple-500 !important;
- color: $gray-50 !important;
- }
-
- &.filter-button-open {
- color: $purple-200 !important;
-
- .filter-icon {
- color: $purple-200 !important;
- }
- }
-
- .filter-icon {
- height: 10px;
- width: 12px;
- color: $gray-50;
- margin-left: 15px;
- }
- }
-
- .filter-panel {
- position: absolute;
- padding-left: 24px;
- padding-right: 24px;
- max-width: 40vw;
- width: 100%;
- z-index: 9999;
- background: $white;
- border-radius: 2px;
- box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
- top: 44px;
- left: 20vw;
- font-size: 14px;
- line-height: 1.43;
- text-overflow: ellipsis;
-
- .tags-category {
- border-bottom: 1px solid $gray-600;
- padding-bottom: 24px;
- padding-top: 24px;
- }
-
- .tags-header {
- flex-basis: 96px;
- flex-shrink: 0;
-
- a {
- font-size: 12px;
- line-height: 1.33;
- color: $blue-10;
- margin-top: 4px;
-
- &:focus, &:hover, &:active {
- text-decoration: underline;
- }
- }
- }
-
- .tag-edit-input {
- border-bottom: 1px solid $gray-500 !important;
-
- &:focus, &:focus ~ .input-group-append {
- border-color: $purple-500 !important;
- }
- }
-
- .new-tag-item {
- width: 100%;
- background-repeat: no-repeat;
- background-position: center left 10px;
- border-bottom: 1px solid $gray-500 !important;
- background-size: 10px 10px;
- padding-left: 40px;
- background-image: url(~client/assets/svg/for-css/positive.svg);
- }
-
- .tag-edit-item .input-group-append {
- border-bottom: 1px solid $gray-500 !important;
-
- &:focus {
- border-color: $purple-500;
- }
- }
-
- .custom-control-label {
- margin-left: 10px;
- }
-
- .filter-panel-footer {
- padding-top: 16px;
- padding-bottom: 16px;
-
- a {
- &:focus, &:hover, &:active {
- text-decoration: underline;
- }
- }
-
- .btn-filters-danger {
- color: $red-50;
- }
-
- .btn-filters-primary {
- color: $blue-10;
- }
-
- .btn-filters-secondary {
- color: $gray-300;
- }
- }
- }
-
- .button-label {
- display: inline-block;
- }
diff --git a/website/client/components/header/notificationsDropdown.vue b/website/client/components/header/notificationsDropdown.vue
index 3132f9e9b9..48f7bba6e7 100644
--- a/website/client/components/header/notificationsDropdown.vue
+++ b/website/client/components/header/notificationsDropdown.vue
@@ -87,6 +87,7 @@ 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 UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems';
import CARD_RECEIVED from './notifications/cardReceived';
@@ -102,7 +103,7 @@ export default {
// One component for each type
NEW_STUFF, GROUP_TASK_NEEDS_WORK,
GUILD_INVITATION, PARTY_INVITATION, CHALLENGE_INVITATION,
- QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED,
+ QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED, GROUP_TASK_ASSIGNED,
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
WorldBoss: WORLD_BOSS,
@@ -118,7 +119,7 @@ export default {
openStatus: undefined,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
- 'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK',
+ 'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK', 'GROUP_TASK_APPROVAL',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
@@ -126,7 +127,7 @@ export default {
handledNotifications: [
'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
- 'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
+ 'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
'VERIFY_USERNAME',
diff --git a/website/client/components/tasks/taskModal.vue b/website/client/components/tasks/taskModal.vue
index 4e9c0aca40..7ef40e3b6d 100644
--- a/website/client/components/tasks/taskModal.vue
+++ b/website/client/components/tasks/taskModal.vue
@@ -159,6 +159,15 @@
| {{ $t(frequency) }}
.option.group-options(v-if='groupId')
+ .form-group(v-if="task.type === 'todo'")
+ label(v-once) {{ $t('sharedCompletion') }}
+ b-dropdown.inline-dropdown(:text="$t(sharedCompletion)")
+ b-dropdown-item(
+ v-for="completionOption in ['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']",
+ :key="completionOption",
+ @click="sharedCompletion = completionOption",
+ :class="{active: sharedCompletion === completionOption}"
+ ) {{ $t(completionOption) }}
.form-group.row
label.col-12(v-once) {{ $t('assignedTo') }}
.col-12.mt-2
@@ -179,23 +188,12 @@
.row
button.btn.btn-primary(@click.stop.prevent="showAssignedSelect = !showAssignedSelect") {{$t('close')}}
-
- .option.group-options(v-if='groupId')
.form-group
label(v-once) {{ $t('approvalRequired') }}
toggle-switch.d-inline-block(
:checked="requiresApproval",
@change="updateRequiresApproval"
)
- .form-group(v-if="task.type === 'todo'")
- label(v-once) {{ $t('sharedCompletion') }}
- b-dropdown.inline-dropdown(:text="$t(sharedCompletion)")
- b-dropdown-item(
- v-for="completionOption in ['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']",
- :key="completionOption",
- @click="sharedCompletion = completionOption",
- :class="{active: sharedCompletion === completionOption}"
- ) {{ $t(completionOption) }}
.advanced-settings(v-if="task.type !== 'reward'")
.advanced-settings-toggle.d-flex.justify-content-between.align-items-center(@click = "showAdvancedOptions = !showAdvancedOptions")
@@ -711,7 +709,7 @@ export default {
calendar: calendarIcon,
}),
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
- sharedCompletion: 'recurringCompletion',
+ sharedCompletion: 'singleCompletion',
members: [],
memberNamesById: {},
assignedMembers: [],
@@ -842,7 +840,7 @@ export default {
});
this.assignedMembers = [];
if (this.task.group && this.task.group.assignedUsers) this.assignedMembers = this.task.group.assignedUsers;
- if (this.task.group) this.sharedCompletion = this.task.group.sharedCompletion || 'recurringCompletion';
+ if (this.task.group) this.sharedCompletion = this.task.group.sharedCompletion || 'singleCompletion';
}
// @TODO: This whole component is mutating a prop and that causes issues. We need to not copy the prop similar to group modals
@@ -926,7 +924,6 @@ export default {
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
if (this.groupId) {
- this.task.group.assignedUsers = this.assignedMembers;
this.task.requiresApproval = this.requiresApproval;
this.task.group.approval.required = this.requiresApproval;
this.task.sharedCompletion = this.sharedCompletion;
@@ -954,6 +951,7 @@ export default {
});
});
Promise.all(promises);
+ this.task.group.assignedUsers = this.assignedMembers;
this.$emit('taskCreated', this.task);
} else {
this.createTask(this.task);
diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json
index 034cb51761..08f7866cbc 100644
--- a/website/common/locales/en/groups.json
+++ b/website/common/locales/en/groups.json
@@ -285,7 +285,8 @@
"claim": "Claim",
"removeClaim": "Remove Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
- "yourTaskHasBeenApproved": "Your task <%= taskText %> has been approved.",
+ "youHaveBeenAssignedTask": "<%= managerName %> has assigned you the task <%= taskText %>.",
+ "yourTaskHasBeenApproved": "Your task <%= taskText %> has been approved.",
"taskNeedsWork": "<%= managerName %> marked <%= taskText %> as needing additional work.",
"userHasRequestedTaskApproval": "<%= user %> requests approval for <%= taskName %>",
"approve": "Approve",
diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json
index cdf0c07044..361be74664 100644
--- a/website/common/locales/en/tasks.json
+++ b/website/common/locales/en/tasks.json
@@ -173,7 +173,7 @@
"habitCounterDown": "Negative Counter (Resets <%= frequency %>)",
"taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested",
"taskApprovalHasBeenRequested": "Approval has been requested",
- "taskApprovalWasNotRequested": "Only a task waiting for approval can be marked as needing more work",
+ "taskApprovalWasNotRequested": "Approval has not been requested for this task.",
"approvals": "Approvals",
"approvalRequired": "Needs Approval",
"repeatZero": "Daily is never due",
diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js
index 69f050ad3d..93ce3cb1eb 100644
--- a/website/server/controllers/api-v3/tasks/groups.js
+++ b/website/server/controllers/api-v3/tasks/groups.js
@@ -206,6 +206,14 @@ api.assignTask = {
let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text});
const newMessage = group.sendChat(message);
promises.push(newMessage.save());
+ } else {
+ const taskText = task.text;
+ const managerName = user.profile.name;
+
+ assignedUser.addNotification('GROUP_TASK_ASSIGNED', {
+ message: res.t('youHaveBeenAssignedTask', {managerName, taskText}),
+ taskId: task._id,
+ });
}
promises.push(group.syncTask(task, assignedUser));
@@ -261,6 +269,15 @@ api.unassignTask = {
await group.unlinkTask(task, assignedUser);
+ let notificationIndex = assignedUser.notifications.findIndex(function findNotification (notification) {
+ return notification && notification.data && notification.type === 'GROUP_TASK_ASSIGNED' && notification.data.taskId === task._id;
+ });
+
+ if (notificationIndex !== -1) {
+ assignedUser.notifications.splice(notificationIndex, 1);
+ await assignedUser.save();
+ }
+
res.respond(200, task);
},
};
@@ -308,6 +325,9 @@ api.approveTask = {
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;
diff --git a/website/server/models/group.js b/website/server/models/group.js
index 53f66e9a3a..fee789a878 100644
--- a/website/server/models/group.js
+++ b/website/server/models/group.js
@@ -1341,7 +1341,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
matchingTask.group.id = taskToSync.group.id;
matchingTask.userId = user._id;
matchingTask.group.taskId = taskToSync._id;
- user.tasksOrder[`${taskToSync.type}s`].push(matchingTask._id);
+ user.tasksOrder[`${taskToSync.type}s`].unshift(matchingTask._id);
} else {
_.merge(matchingTask, syncableAttrs(taskToSync));
// Make sure the task is in user.tasksOrder
@@ -1419,9 +1419,29 @@ schema.methods.removeTask = async function groupRemoveTask (task) {
$set: {'group.broken': 'TASK_DELETED'},
}, {multi: true}).exec();
+ // 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
+
+ // Remove old notifications
+ let removalPromises = [];
+ managers.forEach((manager) => {
+ let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
+ return notification && notification.data && notification.data.groupTaskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
+ });
+
+ if (notificationIndex !== -1) {
+ manager.notifications.splice(notificationIndex, 1);
+ removalPromises.push(manager.save());
+ }
+ });
+
removeFromArray(group.tasksOrder[`${task.type}s`], task._id);
group.markModified('tasksOrder');
- return await group.save();
+ removalPromises.push(group.save());
+
+ return await Promise.all(removalPromises);
};
// Returns true if the user has reached the spam message limit
diff --git a/website/server/models/task.js b/website/server/models/task.js
index 1858cd9623..db7a05e867 100644
--- a/website/server/models/task.js
+++ b/website/server/models/task.js
@@ -113,7 +113,7 @@ export let TaskSchema = new Schema({
requested: {$type: Boolean, default: false},
requestedDate: {$type: Date},
},
- sharedCompletion: {$type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.default},
+ sharedCompletion: {$type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.single},
},
reminders: [{
diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js
index acb280b16a..79075f7d7f 100644
--- a/website/server/models/userNotification.js
+++ b/website/server/models/userNotification.js
@@ -15,6 +15,7 @@ const NOTIFICATION_TYPES = [
'CRON',
'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED',
+ 'GROUP_TASK_ASSIGNED',
'GROUP_TASK_NEEDS_WORK',
'LOGIN_INCENTIVE',
'GROUP_INVITE_ACCEPTED',