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 @@
+
+base-notification(
+ :can-remove="canRemove",
+ :has-icon="false",
+ :notification="notification",
+ :read-after-click="true",
+ @click="action",
+)
+ div(slot="content", v-html="notification.data.message")
+
+
+
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 @@
div
approval-modal(:task='task')
- .claim-bottom-message.col-12
- .task-unclaimed.d-flex.justify-content-between(v-if='!approvalRequested && !multipleApprovalsRequested')
- span {{ message }}
- a.text-right(@click='claim()', v-if='!userIsAssigned') {{ $t('claim') }}
- a.text-right(@click='unassign()', v-if='userIsAssigned') {{ $t('removeClaim') }}
- .row.task-single-approval(v-if='approvalRequested')
- .col-6.text-center
- a(@click='approve()') {{ $t('approveTask') }}
- .col-6.text-center
- a(@click='needsWork()') {{ $t('needsWork') }}
- .text-center.task-multi-approval(v-if='multipleApprovalsRequested')
- a(@click='showRequests()') {{ $t('viewRequests') }}
+ .claim-bottom-message.d-flex.align-items-center(v-if='!approvalRequested && !multipleApprovalsRequested')
+ .mr-auto.ml-2(v-html='message')
+ .ml-auto.mr-2(v-if='!userIsAssigned')
+ a(@click='claim()').claim-color {{ $t('claim') }}
+ .ml-auto.mr-2(v-if='userIsAssigned')
+ a(@click='unassign()').unclaim-color {{ $t('removeClaim') }}
+ .claim-bottom-message.d-flex.align-items-center.justify-content-around(v-if='approvalRequested && userIsManager')
+ a(@click='approve()').approve-color {{ $t('approveTask') }}
+ a(@click='needsWork()') {{ $t('needsWork') }}
+ .claim-bottom-message.d-flex.align-items-center(v-if='multipleApprovalsRequested && userIsManager')
+ a(@click='showRequests()') {{ $t('viewRequests') }}