diff --git a/test/api/v3/integration/groups/POST-group_remove_manager.test.js b/test/api/v3/integration/groups/POST-group_remove_manager.test.js index 0d426b27ae..c07e868009 100644 --- a/test/api/v3/integration/groups/POST-group_remove_manager.test.js +++ b/test/api/v3/integration/groups/POST-group_remove_manager.test.js @@ -21,13 +21,13 @@ describe('POST /group/:groupId/remove-manager', () => { type: groupType, privacy: 'public', }, - members: 1, + members: 2, }); groupToUpdate = group; leader = groupLeader; nonLeader = members[0]; - nonManager = members[0]; + nonManager = members[1]; }); it('returns an error when a non group leader tries to add member', async () => { @@ -71,10 +71,10 @@ describe('POST /group/:groupId/remove-manager', () => { type: 'todo', requiresApproval: true, }); - await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`); - let memberTasks = await leader.get('/tasks/user'); + await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`); + let memberTasks = await nonManager.get('/tasks/user'); let syncedTask = find(memberTasks, findAssignedTask); - await expect(leader.post(`/tasks/${syncedTask._id}/score/up`)) + await expect(nonManager.post(`/tasks/${syncedTask._id}/score/up`)) .to.eventually.be.rejected.and.to.eql({ code: 401, error: 'NotAuthorized', diff --git a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js index edef9c6502..fe831b4452 100644 --- a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js +++ b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js @@ -1,11 +1,10 @@ import { createAndPopulateGroup, - translate as t, } from '../../../../../helpers/api-integration/v3'; import { find } from 'lodash'; describe('GET /approvals/group/:groupId', () => { - let user, guild, member, task, syncedTask; + let user, guild, member, addlMember, task, syncedTask, addlSyncedTask; function findAssignedTask (memberTask) { return memberTask.group.id === guild._id; @@ -17,12 +16,13 @@ describe('GET /approvals/group/:groupId', () => { name: 'Test Guild', type: 'guild', }, - members: 1, + members: 2, }); guild = group; user = groupLeader; member = members[0]; + addlMember = members[1]; task = await user.post(`/tasks/group/${guild._id}`, { text: 'test todo', @@ -31,37 +31,46 @@ describe('GET /approvals/group/:groupId', () => { }); await user.post(`/tasks/${task._id}/assign/${member._id}`); + await user.post(`/tasks/${task._id}/assign/${addlMember._id}`); let memberTasks = await member.get('/tasks/user'); syncedTask = find(memberTasks, findAssignedTask); + let 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('errors when user is not the group leader', async () => { - await expect(member.get(`/approvals/group/${guild._id}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('onlyGroupLeaderCanEditTasks'), - }); + it('provides only user\'s own tasks when user is not the group leader', async () => { + let approvals = await member.get(`/approvals/group/${guild._id}`); + expect(approvals[0]._id).to.equal(syncedTask._id); + expect(approvals[1]).to.not.exist; }); - it('gets a list of task that need approval', async () => { + it('allows group leaders to get a list of tasks that need approval', async () => { let 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 task that need approval', async () => { + it('allows managers to get a list of tasks that need approval', async () => { await user.post(`/groups/${guild._id}/add-manager`, { managerId: member._id, }); let approvals = await member.get(`/approvals/group/${guild._id}`); expect(approvals[0]._id).to.equal(syncedTask._id); + expect(approvals[1]._id).to.equal(addlSyncedTask._id); }); }); 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 45359c2fce..9364ea11d8 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 @@ -93,6 +93,25 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { 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(); + let groupTask = await user.get(`/tasks/group/${guild._id}`); + + expect(user.notifications.length).to.equal(2); // includes Guild Joined achievement + expect(user.notifications[1].type).to.equal('GROUP_TASK_CLAIMED'); + expect(user.notifications[1].data.taskId).to.equal(groupTask[0]._id); + expect(user.notifications[1].data.groupId).to.equal(guild._id); + expect(member2.notifications.length).to.equal(1); + expect(member2.notifications[0].type).to.equal('GROUP_TASK_CLAIMED'); + expect(member2.notifications[0].data.taskId).to.equal(groupTask[0]._id); + expect(member2.notifications[0].data.groupId).to.equal(guild._id); + }); + it('assigns a task to a user', async () => { await user.post(`/tasks/${task._id}/assign/${member._id}`); diff --git a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js index bf073cf251..aa2b3876a6 100644 --- a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js +++ b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js @@ -56,11 +56,11 @@ describe('PUT /tasks/:id', () => { requiresApproval: true, }); - let memberTasks = await member.get('/tasks/user'); + let memberTasks = await member2.get('/tasks/user'); let syncedTask = find(memberTasks, (memberTask) => memberTask.group.taskId === task._id); // score up to trigger approval - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) + await expect(member2.post(`/tasks/${syncedTask._id}/score/up`)) .to.eventually.be.rejected.and.to.eql({ code: 401, error: 'NotAuthorized', diff --git a/test/client/unit/specs/store/getters/tasks/getTaskClasses.js b/test/client/unit/specs/store/getters/tasks/getTaskClasses.js index c1ccca379d..b0c25dec7b 100644 --- a/test/client/unit/specs/store/getters/tasks/getTaskClasses.js +++ b/test/client/unit/specs/store/getters/tasks/getTaskClasses.js @@ -109,12 +109,13 @@ describe('getTaskClasses getter', () => { }); }); - xit('returns good todo classes', () => { + it('returns good todo classes', () => { const task = {type: 'todo', value: 2}; expect(getTaskClasses(task, 'control')).to.deep.equal({ bg: 'task-good-control-bg', checkbox: 'task-good-control-checkbox', - inner: 'task-good-control-inner-daily-todo`', + inner: 'task-good-control-inner-daily-todo', + icon: 'task-good-control-icon', }); }); @@ -140,4 +141,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'}}; + expect(getTaskClasses(task, 'control')).to.deep.equal({ + bg: 'task-good-control-bg-noninteractive', + checkbox: 'task-good-control-checkbox', + inner: 'task-good-control-inner-daily-todo', + icon: 'task-good-control-icon', + }); + }); }); diff --git a/website/client/assets/scss/button.scss b/website/client/assets/scss/button.scss index a278ded2d1..bc649e1dc5 100644 --- a/website/client/assets/scss/button.scss +++ b/website/client/assets/scss/button.scss @@ -74,10 +74,10 @@ } .btn-success { - background: $green-10; + background: $green-100; &:disabled { - background: $green-10; + background: $green-100; } &:hover:not(:disabled), &:active, &.active { diff --git a/website/client/assets/scss/task.scss b/website/client/assets/scss/task.scss index f7fc28751a..f969a41015 100644 --- a/website/client/assets/scss/task.scss +++ b/website/client/assets/scss/task.scss @@ -8,6 +8,9 @@ .daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; } } } + &-bg-noninteractive { + background: $maroon-100 !important; + } &-inner-habit { background: rgba(26, 24, 29, 0.24) !important; } &-inner-daily-todo { background: $maroon-500 !important; } &-checkbox { color: $maroon-100 !important; } @@ -38,6 +41,9 @@ .daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; } } } + &-bg-noninteractive { + background: $red-100 !important; + } &-inner-habit { background: rgba(26, 24, 29, 0.24) !important; } &-inner-daily-todo { background: $red-500 !important; } &-checkbox { color: $red-100 !important; } @@ -69,6 +75,9 @@ .daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; } } } + &-bg-noninteractive { + background: $orange-100 !important; + } &-inner-habit { background: rgba(183, 90, 28, 0.4) !important; } &-inner-daily-todo { background: $orange-500 !important; } &-checkbox { color: $orange-100 !important; } @@ -109,6 +118,9 @@ .daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; } } } + &-bg-noninteractive { + background: $yellow-100 !important; + } &-inner-habit { background: rgba(183, 90, 28, 0.32) !important; } &-inner-daily-todo { background: $yellow-500 !important; } &-checkbox { color: $yellow-100 !important; } @@ -149,6 +161,9 @@ .daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; } } } + &-bg-noninteractive { + background: $green-100 !important; + } &-inner-habit { background: rgba(26, 24, 29, 0.24) !important; } &-inner-daily-todo { background: #77f4c7 !important; } &-checkbox { color: $green-10 !important; } @@ -179,6 +194,9 @@ .daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; } } } + &-bg-noninteractive { + background: $teal-100 !important; + } &-inner-habit { background: rgba(26, 24, 29, 0.24) !important; } &-inner-daily-todo { background: #8dedf6 !important; } &-checkbox { color: $teal-100 !important; } @@ -209,6 +227,9 @@ .daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; } } } + &-bg-noninteractive { + background: $blue-100 !important; + } &-inner-habit { background: rgba(26, 24, 29, 0.24) !important; } &-inner-daily-todo { background: $blue-500 !important; } &-checkbox { color: $blue-100 !important; } @@ -267,6 +288,10 @@ &:hover { background: rgba(255, 217, 160, 0.48) !important; } } + &-bg-noninteractive { + background: rgba(255, 217, 160, 0.32) !important; + .small-text { color: $orange-10 !important; } + } } } diff --git a/website/client/components/group-plans/taskInformation.vue b/website/client/components/group-plans/taskInformation.vue index fbe4516d4f..86f29dc5b7 100644 --- a/website/client/components/group-plans/taskInformation.vue +++ b/website/client/components/group-plans/taskInformation.vue @@ -205,8 +205,6 @@ export default { }); }, async loadApprovals () { - if (this.group.leader._id !== this.user._id) return []; - let approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', { groupId: this.searchId, }); diff --git a/website/client/components/groups/group.vue b/website/client/components/groups/group.vue index 8ff4d05b69..9416b5f3cd 100644 --- a/website/client/components/groups/group.vue +++ b/website/client/components/groups/group.vue @@ -349,25 +349,6 @@ export default { isMember () { return this.isMemberOfGroup(this.user, this.group); }, - memberProfileName (memberId) { - let foundMember = find(this.group.members, function findMember (member) { - return member._id === memberId; - }); - return foundMember.profile.name; - }, - isManager (memberId, group) { - return Boolean(group.managers[memberId]); - }, - userCanApprove (userId, group) { - if (!group) return false; - let leader = group.leader._id === userId; - let userIsManager = Boolean(group.managers[userId]); - return leader || userIsManager; - }, - hasChallenges () { - if (!this.group.challenges) return false; - return this.group.challenges.length === 0; - }, showNoNotificationsMessage () { return this.group.memberCount > this.$store.state.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF; }, diff --git a/website/client/components/header/menu.vue b/website/client/components/header/menu.vue index 8b2c37354d..7b77ed7c42 100644 --- a/website/client/components/header/menu.vue +++ b/website/client/components/header/menu.vue @@ -503,7 +503,9 @@ export default { let droppedElement = document.getElementsByClassName('down')[0]; if (droppedElement && droppedElement !== element) { droppedElement.classList.remove('down'); - droppedElement.lastChild.style.maxHeight = 0; + if (droppedElement.lastChild) { + droppedElement.lastChild.style.maxHeight = 0; + } } element.classList.toggle('down'); diff --git a/website/client/components/header/notifications/groupTaskClaimed.vue b/website/client/components/header/notifications/groupTaskClaimed.vue new file mode 100644 index 0000000000..508177dbb2 --- /dev/null +++ b/website/client/components/header/notifications/groupTaskClaimed.vue @@ -0,0 +1,27 @@ + + + diff --git a/website/client/components/header/notificationsDropdown.vue b/website/client/components/header/notificationsDropdown.vue index 526bbece0b..a325c054b9 100644 --- a/website/client/components/header/notificationsDropdown.vue +++ b/website/client/components/header/notificationsDropdown.vue @@ -88,6 +88,7 @@ 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'; import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems'; import CARD_RECEIVED from './notifications/cardReceived'; @@ -106,7 +107,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, GROUP_TASK_ASSIGNED, + QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED, GROUP_TASK_ASSIGNED, GROUP_TASK_CLAIMED, UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED, NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE, ACHIEVEMENT_JUST_ADD_WATER, ACHIEVEMENT_LOST_MASTERCLASSER, ACHIEVEMENT_MIND_OVER_MATTER, @@ -131,7 +132,7 @@ export default { handledNotifications: [ 'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK', 'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION', - 'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED', + 'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED', 'GROUP_TASK_CLAIMED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', 'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS', 'ACHIEVEMENT_JUST_ADD_WATER', 'ACHIEVEMENT_LOST_MASTERCLASSER', 'ACHIEVEMENT_MIND_OVER_MATTER', diff --git a/website/client/components/tasks/approvalFooter.vue b/website/client/components/tasks/approvalFooter.vue index 94b13f7b3a..55c7f0db3f 100644 --- a/website/client/components/tasks/approvalFooter.vue +++ b/website/client/components/tasks/approvalFooter.vue @@ -1,34 +1,41 @@