mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-15 12:07:43 +00:00
Teams Updates 201908 (#11347)
* fix(teams): no hover bg change for noninteractive checkboxes * feat(teams): send notification to managers on task claim Also fix client unit test broken by prev commit * feat(groups): don't penalize for tasks assigned since last activity * fix(tests): actually fix client unit * fix(teams): improve task styles * fix(teams): let people other than leader see relevant approvals Also more style fixes * fix(approvals): better filtering and task headings for approval data * fix(test): correct test expectations for new GET /approvals behavior * fix(groups): style tweaks * different border for group and normal tasks * fix(teams): remove extra click for claiming * fix(teams): leaders & managers can check off approval-required tasks * fix(teams): don't notify user of own claim * fix group task margin and z-index on hover * fix(menu): sporadic error in top bar * fix(teams): more approval header and footer adjustments * fix(tests): adjust expectations for self-approval * fix(teams): address PR comments * refactor(timestamps): date user activity on authenticated requests * refactor(timestamps): update local user instead of direct db update
This commit is contained in:
parent
5f2032a9d5
commit
01d272d2c4
26 changed files with 314 additions and 145 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,10 +74,10 @@
|
|||
}
|
||||
|
||||
.btn-success {
|
||||
background: $green-10;
|
||||
background: $green-100;
|
||||
|
||||
&:disabled {
|
||||
background: $green-10;
|
||||
background: $green-100;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled), &:active, &.active {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action",
|
||||
)
|
||||
div(slot="content", v-html="notification.data.message")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.groupId;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,34 +1,41 @@
|
|||
<template lang="pug">
|
||||
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') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss", scoped>
|
||||
.claim-bottom-message {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.task-unclaimed {
|
||||
span {
|
||||
margin-right: 0.25rem;
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
.claim-bottom-message {
|
||||
background-color: $gray-700;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
color: $gray-200;
|
||||
font-size: 12px;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
.approve-color {
|
||||
color: $green-10 !important;
|
||||
}
|
||||
.claim-color {
|
||||
color: $blue-10 !important;
|
||||
}
|
||||
.unclaim-color {
|
||||
color: $red-50 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
@ -76,8 +83,11 @@ export default {
|
|||
return this.$t('taskIsUnassigned');
|
||||
}
|
||||
},
|
||||
userIsManager () {
|
||||
if (this.group && (this.group.leader.id === this.user._id || this.group.managers[this.user._id])) return true;
|
||||
},
|
||||
approvalRequested () {
|
||||
if (this.task.approvals && this.task.approvals.length === 1) return true;
|
||||
if (this.task.approvals && this.task.approvals.length === 1 || this.task.group && this.task.group.approval && this.task.group.approval.requested) return true;
|
||||
},
|
||||
multipleApprovalsRequested () {
|
||||
if (this.task.approvals && this.task.approvals.length > 1) return true;
|
||||
|
|
@ -85,8 +95,6 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
async claim () {
|
||||
if (!confirm(this.$t('confirmClaim'))) return;
|
||||
|
||||
let taskId = this.task._id;
|
||||
// If we are on the user task
|
||||
if (this.task.userId) {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
<template lang="pug">
|
||||
.claim-bottom-message.col-12.text-center(v-if='task.approvals && task.approvals.length > 0', :class="{approval: userIsAdmin}")
|
||||
.task-unclaimed
|
||||
| {{ message }}
|
||||
.claim-top-message.d-flex.align-content-center(v-if='message', :class="{'approval-action': userIsAdmin, 'approval-pending': !userIsAdmin}")
|
||||
.m-auto(v-html='message')
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.claim-bottom-message {
|
||||
z-index: 9;
|
||||
}
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
.claim-top-message {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.approval {
|
||||
background: #24cc8f;
|
||||
color: #fff;
|
||||
}
|
||||
.approval-action {
|
||||
background: $green-100;
|
||||
}
|
||||
|
||||
.approval-pending {
|
||||
background: $gray-300;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
@ -22,20 +31,22 @@ export default {
|
|||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
message () {
|
||||
let approvals = this.task.approvals;
|
||||
let approvals = this.task.approvals || [];
|
||||
let approvalsLength = approvals.length;
|
||||
let userIsRequesting = this.task.group.approvals && this.task.group.approvals.indexOf(this.user._id) !== -1;
|
||||
let userIsRequesting = approvals.findIndex((approval) => {
|
||||
return approval.userId.id === this.user._id;
|
||||
}) !== -1;
|
||||
|
||||
if (approvalsLength === 1 && !userIsRequesting) {
|
||||
return this.$t('userRequestsApproval', {userName: approvals[0].userId.profile.name});
|
||||
} else if (approvalsLength > 1 && !userIsRequesting) {
|
||||
return this.$t('userCountRequestsApproval', {userCount: approvalsLength});
|
||||
} else if (approvalsLength === 1 && userIsRequesting) {
|
||||
} else if (approvalsLength === 1 && userIsRequesting || this.task.group.approval && this.task.group.approval.requested && !this.task.group.approval.approved) {
|
||||
return this.$t('youAreRequestingApproval');
|
||||
}
|
||||
},
|
||||
userIsAdmin () {
|
||||
return this.group.leader.id === this.user._id;
|
||||
return this.group && (this.group.leader.id === this.user._id || this.group.managers[this.user._id]);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<template lang="pug">
|
||||
.task-wrapper
|
||||
.task(@click='castEnd($event, task)', :class="`type_${task.type}`")
|
||||
approval-header(:task='task', v-if='this.task.group.id', :group='group')
|
||||
.task(@click='castEnd($event, task)', :class="[{'groupTask': task.group.id}, `type_${task.type}`]")
|
||||
approval-header(:task='task', v-if='task.group.id', :group='group')
|
||||
.d-flex(:class="{'task-not-scoreable': isUser !== true}")
|
||||
// Habits left side control
|
||||
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.up.bg")
|
||||
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="[{'control-bottom-box': this.task.group.id, 'control-top-box': approvalsClass}, controlClass.up.bg]")
|
||||
.task-control.habit-control(:class="controlClass.up.inner", @click="(isUser && task.up) ? score('up') : null")
|
||||
.svg-icon.lock(v-if="this.task.group.id && !isUser", v-html="icons.lock", :class="controlClass.up.icon")
|
||||
.svg-icon.positive(v-else, v-html="icons.positive")
|
||||
// Dailies and todos left side control
|
||||
.left-control.d-flex.justify-content-center(v-if="task.type === 'daily' || task.type === 'todo'", :class="controlClass.bg")
|
||||
.left-control.d-flex.justify-content-center(v-if="task.type === 'daily' || task.type === 'todo'", :class="[{'control-bottom-box': this.task.group.id, 'control-top-box': approvalsClass}, controlClass.bg]")
|
||||
.task-control.daily-todo-control(:class="controlClass.inner", @click="isUser ? score(task.completed ? 'down' : 'up') : null")
|
||||
.svg-icon.lock(v-html="icons.lock", v-if="this.task.group.id && !isUser && !task.completed", :class="controlClass.icon")
|
||||
.svg-icon.check(v-else, v-html="icons.check", :class="{'display-check-icon': task.completed, [controlClass.checkbox]: true}")
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
.tag-label(v-for="tag in getTagsFor(task)", v-markdown="tag")
|
||||
|
||||
// Habits right side control
|
||||
.right-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.down.bg")
|
||||
.right-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="[{'control-bottom-box': this.task.group.id, 'control-top-box': approvalsClass}, controlClass.down.bg]")
|
||||
.task-control.habit-control(:class="controlClass.down.inner", @click="(isUser && task.down) ? score('down') : null")
|
||||
.svg-icon.lock(v-if="this.task.group.id && !isUser", v-html="icons.lock", :class="controlClass.down.icon")
|
||||
.svg-icon.negative(v-else, v-html="icons.negative")
|
||||
|
|
@ -107,12 +107,23 @@
|
|||
.right-control.d-flex.align-items-center.justify-content-center.reward-control(v-if="task.type === 'reward'", :class="controlClass.bg", @click="isUser ? score('down') : null")
|
||||
.svg-icon(v-html="icons.gold")
|
||||
.small-text {{task.value}}
|
||||
approval-footer(:task='task', v-if='this.task.group.id', :group='group')
|
||||
approval-footer(:task='task', v-if='task.group.id', :group='group')
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.control-bottom-box {
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
}
|
||||
|
||||
.control-top-box {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
}
|
||||
|
||||
|
||||
.task {
|
||||
margin-bottom: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
|
|
@ -122,13 +133,29 @@
|
|||
|
||||
&:hover {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.task:not(.groupTask) {
|
||||
&:hover {
|
||||
.left-control, .right-control, .task-content {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task.groupTask {
|
||||
|
||||
&:hover {
|
||||
border: $purple-400 solid 1px;
|
||||
border-radius: 3px;
|
||||
margin: -1px; // to counter the border width
|
||||
margin-bottom: 1px;
|
||||
transition: none; // with transition, the border color switches from black to $purple-400
|
||||
}
|
||||
}
|
||||
|
||||
.task-habit-disabled-control-habit:hover {
|
||||
cursor: initial;
|
||||
}
|
||||
|
|
@ -622,6 +649,9 @@ export default {
|
|||
if (task.type === 'habit') return true;
|
||||
return false;
|
||||
},
|
||||
approvalsClass () {
|
||||
return this.group && this.task.approvals && this.task.approvals.length > 0;
|
||||
},
|
||||
controlClass () {
|
||||
return this.getTaskClasses(this.task, 'control', this.dueDate);
|
||||
},
|
||||
|
|
@ -744,6 +774,16 @@ export default {
|
|||
const user = this.user;
|
||||
const task = this.task;
|
||||
|
||||
if (task.group.approval.required) {
|
||||
task.group.approval.requested = true;
|
||||
const groupResponse = await axios.get(`/api/v4/groups/${task.group.id}`);
|
||||
let managers = Object.keys(groupResponse.data.data.managers);
|
||||
managers.push(groupResponse.data.data.leader._id);
|
||||
if (managers.indexOf(user._id) !== -1) {
|
||||
task.group.approval.approved = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
scoreTask({task, user, direction});
|
||||
} catch (err) {
|
||||
|
|
@ -767,8 +807,6 @@ export default {
|
|||
}
|
||||
|
||||
|
||||
if (task.group.approval.required) task.group.approval.requested = true;
|
||||
|
||||
Analytics.updateUser();
|
||||
const response = await axios.post(`/api/v4/tasks/${task._id}/score/${direction}`);
|
||||
const tmp = response.data.data._tmp || {}; // used to notify drops, critical hits and other bonuses
|
||||
|
|
|
|||
|
|
@ -144,22 +144,22 @@ export function getTaskClasses (store) {
|
|||
}
|
||||
|
||||
return {
|
||||
bg: `task-${color}-control-bg`,
|
||||
bg: task.group && task.group.id && !task.userId ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
|
||||
checkbox: `task-${color}-control-checkbox`,
|
||||
inner: `task-${color}-control-inner-daily-todo`,
|
||||
icon: `task-${color}-control-icon`,
|
||||
};
|
||||
} else if (type === 'reward') {
|
||||
return {
|
||||
bg: 'task-reward-control-bg',
|
||||
bg: task.group && task.group.id && !task.userId ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg',
|
||||
};
|
||||
} else if (type === 'habit') {
|
||||
return {
|
||||
up: task.up ?
|
||||
{ bg: `task-${color}-control-bg`, inner: `task-${color}-control-inner-habit`, icon: `task-${color}-control-icon`} :
|
||||
{ bg: task.group && task.group.id && !task.userId ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, inner: `task-${color}-control-inner-habit`, icon: `task-${color}-control-icon`} :
|
||||
{ bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
|
||||
down: task.down ?
|
||||
{ bg: `task-${color}-control-bg`, inner: `task-${color}-control-inner-habit`, icon: `task-${color}-control-icon`} :
|
||||
{ bg: task.group && task.group.id && !task.userId ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, inner: `task-${color}-control-inner-habit`, icon: `task-${color}-control-icon`} :
|
||||
{ bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,17 +253,17 @@
|
|||
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
||||
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
|
||||
"assignedTo": "Assigned To",
|
||||
"assignedToUser": "Assigned to <%= userName %>",
|
||||
"assignedToMembers": "Assigned to <%= userCount %> members",
|
||||
"assignedToYouAndMembers": "Assigned to you and <%= userCount %> members",
|
||||
"assignedToUser": "Assigned to <strong><%= userName %></strong>",
|
||||
"assignedToMembers": "Assigned to <strong><%= userCount %> members</strong>",
|
||||
"assignedToYouAndMembers": "Assigned to you and <strong><%= userCount %> members</strong>",
|
||||
"youAreAssigned": "You are assigned to this task",
|
||||
"taskIsUnassigned": "This task is unassigned",
|
||||
"confirmClaim": "Are you sure you want to claim this task?",
|
||||
"confirmUnClaim": "Are you sure you want to unclaim this task?",
|
||||
"confirmApproval": "Are you sure you want to approve this task?",
|
||||
"confirmNeedsWork": "Are you sure you want to mark this task as needing work?",
|
||||
"userRequestsApproval": "<%= userName %> requests approval",
|
||||
"userCountRequestsApproval": "<%= userCount %> members request approval",
|
||||
"userRequestsApproval": "<strong><%= userName %></strong> requests approval",
|
||||
"userCountRequestsApproval": "<strong><%= userCount %> members</strong> request approval",
|
||||
"youAreRequestingApproval": "You are requesting approval",
|
||||
"chatPrivilegesRevoked": "You cannot do this because your chat privileges have been removed. For details or to ask if your privileges can be returned, please email our Community Manager at admin@habitica.com or ask your parent or guardian to email them. Please include your @Username in the email. If a moderator has already told you that your chat ban is temporary, you do not need to send an email.",
|
||||
"newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
|
||||
|
|
@ -279,11 +279,12 @@
|
|||
"groupHomeTitle": "Home",
|
||||
"assignTask": "Assign Task",
|
||||
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
|
||||
"claim": "Claim",
|
||||
"claim": "Claim Task",
|
||||
"removeClaim": "Remove Claim",
|
||||
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
|
||||
"youHaveBeenAssignedTask": "<%= managerName %> has assigned you the task <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green notification-bold\"><%= taskText %></span> has been approved.",
|
||||
"taskClaimed": "<%= userName %> has claimed the task <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>",
|
||||
"approve": "Approve",
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ module.exports = function scoreTask (options = {}, req = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
req.yesterDailyScored = task.yesterDailyScored;
|
||||
updateStats(user, stats, req);
|
||||
return [delta];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -567,41 +567,48 @@ api.scoreTask = {
|
|||
}
|
||||
|
||||
if (task.group.approval.required && !task.group.approval.approved) {
|
||||
if (task.group.approval.requested) {
|
||||
throw new NotAuthorized(res.t('taskRequiresApproval'));
|
||||
}
|
||||
|
||||
task.group.approval.requested = true;
|
||||
task.group.approval.requestedDate = new Date();
|
||||
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
|
||||
// @TODO: we can use the User.pushNotification function because we need to ensure notifications are translated
|
||||
let managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
let managers = await User.find({_id: managerIds}, 'notifications preferences').exec(); // Use this method so we can get access to notifications
|
||||
|
||||
let managerPromises = [];
|
||||
managers.forEach((manager) => {
|
||||
manager.addNotification('GROUP_TASK_APPROVAL', {
|
||||
message: res.t('userHasRequestedTaskApproval', {
|
||||
user: user.profile.name,
|
||||
taskName: task.text,
|
||||
}, manager.preferences.language),
|
||||
groupId: group._id,
|
||||
taskId: task._id, // user task id, used to match the notification when the task is approved
|
||||
userId: user._id,
|
||||
groupTaskId: task.group.taskId, // the original task id
|
||||
direction,
|
||||
if (managerIds.indexOf(user._id) !== -1) {
|
||||
task.group.approval.approved = true;
|
||||
task.group.approval.requested = true;
|
||||
task.group.approval.requestedDate = new Date();
|
||||
} else {
|
||||
if (task.group.approval.requested) {
|
||||
throw new NotAuthorized(res.t('taskRequiresApproval'));
|
||||
}
|
||||
|
||||
task.group.approval.requested = true;
|
||||
task.group.approval.requestedDate = new Date();
|
||||
|
||||
let managers = await User.find({_id: managerIds}, 'notifications preferences').exec(); // Use this method so we can get access to notifications
|
||||
|
||||
// @TODO: we can use the User.pushNotification function because we need to ensure notifications are translated
|
||||
let managerPromises = [];
|
||||
managers.forEach((manager) => {
|
||||
manager.addNotification('GROUP_TASK_APPROVAL', {
|
||||
message: res.t('userHasRequestedTaskApproval', {
|
||||
user: user.profile.name,
|
||||
taskName: task.text,
|
||||
}, manager.preferences.language),
|
||||
groupId: group._id,
|
||||
taskId: task._id, // user task id, used to match the notification when the task is approved
|
||||
userId: user._id,
|
||||
groupTaskId: task.group.taskId, // the original task id
|
||||
direction,
|
||||
});
|
||||
managerPromises.push(manager.save());
|
||||
});
|
||||
managerPromises.push(manager.save());
|
||||
});
|
||||
|
||||
managerPromises.push(task.save());
|
||||
await Promise.all(managerPromises);
|
||||
managerPromises.push(task.save());
|
||||
await Promise.all(managerPromises);
|
||||
|
||||
throw new NotAuthorized(res.t('taskApprovalHasBeenRequested'));
|
||||
throw new NotAuthorized(res.t('taskApprovalHasBeenRequested'));
|
||||
}
|
||||
}
|
||||
|
||||
let wasCompleted = task.completed;
|
||||
|
|
|
|||
|
|
@ -200,13 +200,25 @@ api.assignTask = {
|
|||
if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
let promises = [];
|
||||
const taskText = task.text;
|
||||
const userName = user.profile.name;
|
||||
|
||||
if (user._id !== assignedUserId) {
|
||||
const taskText = task.text;
|
||||
const managerName = user.profile.name;
|
||||
|
||||
if (user._id === assignedUserId) {
|
||||
const managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
const managers = await User.find({_id: managerIds}, 'notifications preferences').exec();
|
||||
managers.forEach((manager) => {
|
||||
if (manager._id === user._id) return;
|
||||
manager.addNotification('GROUP_TASK_CLAIMED', {
|
||||
message: res.t('taskClaimed', {userName, taskText}, manager.preferences.language),
|
||||
groupId: group._id,
|
||||
taskId: task._id,
|
||||
});
|
||||
promises.push(manager.save());
|
||||
});
|
||||
} else {
|
||||
assignedUser.addNotification('GROUP_TASK_ASSIGNED', {
|
||||
message: res.t('youHaveBeenAssignedTask', {managerName, taskText}),
|
||||
message: res.t('youHaveBeenAssignedTask', {managerName: userName, taskText}),
|
||||
taskId: task._id,
|
||||
});
|
||||
}
|
||||
|
|
@ -504,15 +516,26 @@ api.getGroupApprovals = {
|
|||
let group = await Group.getGroup({user, groupId, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
let approvals = await Tasks.Task.find({
|
||||
'group.id': groupId,
|
||||
'group.approval.approved': false,
|
||||
'group.approval.requested': true,
|
||||
}, 'userId group text')
|
||||
.populate('userId', 'profile')
|
||||
.exec();
|
||||
let approvals;
|
||||
if (canNotEditTasks(group, user)) {
|
||||
approvals = await Tasks.Task.find({
|
||||
'group.id': groupId,
|
||||
'group.approval.approved': false,
|
||||
'group.approval.requested': true,
|
||||
'group.assignedUsers': user._id,
|
||||
userId: user._id,
|
||||
}, 'userId group text')
|
||||
.populate('userId', 'profile')
|
||||
.exec();
|
||||
} else {
|
||||
approvals = await Tasks.Task.find({
|
||||
'group.id': groupId,
|
||||
'group.approval.approved': false,
|
||||
'group.approval.requested': true,
|
||||
}, 'userId group text')
|
||||
.populate('userId', 'profile')
|
||||
.exec();
|
||||
}
|
||||
|
||||
res.respond(200, approvals);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ export function cron (options = {}) {
|
|||
let todoTally = 0;
|
||||
|
||||
tasksByType.todos.forEach(task => { // make uncompleted To-Dos redder (further incentive to complete them)
|
||||
if (task.group.assignedDate && moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)) return;
|
||||
scoreTask({
|
||||
task,
|
||||
user,
|
||||
|
|
@ -308,6 +309,7 @@ export function cron (options = {}) {
|
|||
if (!user.party.quest.progress.down) user.party.quest.progress.down = 0;
|
||||
|
||||
tasksByType.dailys.forEach((task) => {
|
||||
if (task.group.assignedDate && moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)) return;
|
||||
let completed = task.completed;
|
||||
// Deduct points for missed Daily tasks
|
||||
let EvadeTask = 0;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export function authWithHeaders (options = {}) {
|
|||
res.locals.user = user;
|
||||
req.session.userId = user._id;
|
||||
stackdriverTraceUserId(user._id);
|
||||
|
||||
user.auth.timestamps.updated = new Date();
|
||||
return next();
|
||||
})
|
||||
.catch(next);
|
||||
|
|
@ -108,6 +108,7 @@ export function authWithSession (req, res, next) {
|
|||
|
||||
res.locals.user = user;
|
||||
stackdriverTraceUserId(user._id);
|
||||
user.auth.timestamps.updated = new Date();
|
||||
return next();
|
||||
})
|
||||
.catch(next);
|
||||
|
|
|
|||
|
|
@ -1430,6 +1430,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
|||
matchingTask.group.id = taskToSync.group.id;
|
||||
matchingTask.userId = user._id;
|
||||
matchingTask.group.taskId = taskToSync._id;
|
||||
matchingTask.group.assignedDate = new Date();
|
||||
user.tasksOrder[`${taskToSync.type}s`].unshift(matchingTask._id);
|
||||
} else {
|
||||
_.merge(matchingTask, syncableAttrs(taskToSync));
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export let TaskSchema = new Schema({
|
|||
id: {$type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid.']},
|
||||
broken: {$type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED']},
|
||||
assignedUsers: [{$type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}],
|
||||
assignedDate: {$type: Date},
|
||||
taskId: {$type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid.']},
|
||||
approval: {
|
||||
required: {$type: Boolean, default: false},
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ let schema = new Schema({
|
|||
timestamps: {
|
||||
created: {$type: Date, default: Date.now},
|
||||
loggedin: {$type: Date, default: Date.now},
|
||||
updated: {$type: Date, default: Date.now},
|
||||
},
|
||||
},
|
||||
// We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const NOTIFICATION_TYPES = [
|
|||
'GROUP_TASK_APPROVAL',
|
||||
'GROUP_TASK_APPROVED',
|
||||
'GROUP_TASK_ASSIGNED',
|
||||
'GROUP_TASK_CLAIMED',
|
||||
'GROUP_TASK_NEEDS_WORK',
|
||||
'LOGIN_INCENTIVE',
|
||||
'GROUP_INVITE_ACCEPTED',
|
||||
|
|
|
|||
Loading…
Reference in a new issue