mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +01:00
Merge branch 'develop' into release
This commit is contained in:
@@ -140,4 +140,89 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
message: t('canOnlyApproveTaskOnce'),
|
||||
});
|
||||
});
|
||||
|
||||
it('completes master task when single-completion task is approved', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
|
||||
let masterTask = find(groupTasks, (groupTask) => {
|
||||
return groupTask._id === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
});
|
||||
|
||||
it('deletes other assigned user tasks when single-completion task is approved', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
let member2Tasks = await member2.get('/tasks/user');
|
||||
|
||||
let syncedTask2 = find(member2Tasks, (memberTask) => {
|
||||
return memberTask.group.taskId === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(syncedTask2).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
let masterTask = find(groupTasks, (groupTask) => {
|
||||
return groupTask._id === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(masterTask.completed).to.equal(false);
|
||||
});
|
||||
|
||||
it('completes master task when all user tasks are approved if all assigned must complete', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
|
||||
|
||||
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
|
||||
let masterTask = find(groupTasks, (groupTask) => {
|
||||
return groupTask._id === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to score an apporoved task', async () => {
|
||||
it('allows a user to score an approved task', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
@@ -137,4 +137,112 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(updatedTask.completed).to.equal(true);
|
||||
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('completes master task when single-completion task is completed', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
|
||||
let syncedTask = find(memberTasks, (memberTask) => {
|
||||
return memberTask.group.taskId === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
let masterTask = find(groupTasks, (groupTask) => {
|
||||
return groupTask._id === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
});
|
||||
|
||||
it('deletes other assigned user tasks when single-completion task is completed', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'singleCompletion',
|
||||
});
|
||||
|
||||
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, (memberTask) => {
|
||||
return memberTask.group.taskId === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
let member2Tasks = await member2.get('/tasks/user');
|
||||
|
||||
let syncedTask2 = find(member2Tasks, (memberTask) => {
|
||||
return memberTask.group.taskId === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(syncedTask2).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
|
||||
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, (memberTask) => {
|
||||
return memberTask.group.taskId === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
let masterTask = find(groupTasks, (groupTask) => {
|
||||
return groupTask._id === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(masterTask.completed).to.equal(false);
|
||||
});
|
||||
|
||||
it('completes master task when all user tasks are completed if all assigned must complete', async () => {
|
||||
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'shared completion todo',
|
||||
type: 'todo',
|
||||
requiresApproval: false,
|
||||
sharedCompletion: 'allAssignedCompletion',
|
||||
});
|
||||
|
||||
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 member2Tasks = await member2.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, (memberTask) => {
|
||||
return memberTask.group.taskId === sharedCompletionTask._id;
|
||||
});
|
||||
let syncedTask2 = find(member2Tasks, (memberTask) => {
|
||||
return memberTask.group.taskId === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${syncedTask2._id}/score/up`);
|
||||
|
||||
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
let masterTask = find(groupTasks, (groupTask) => {
|
||||
return groupTask._id === sharedCompletionTask._id;
|
||||
});
|
||||
|
||||
expect(masterTask.completed).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ div
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
/* @TODO: The modal-open class is not being removed. Let's try this for now */
|
||||
.modal, .modal-open {
|
||||
.modal {
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
@@ -499,8 +499,16 @@ export default {
|
||||
});
|
||||
|
||||
this.$root.$on('bv::modal::hidden', (bvEvent) => {
|
||||
const modalId = bvEvent.target && bvEvent.target.id;
|
||||
if (!modalId) return;
|
||||
let modalId = bvEvent.target && bvEvent.target.id;
|
||||
|
||||
// sometimes the target isn't passed to the hidden event, fallback is the vueTarget
|
||||
if (!modalId) {
|
||||
modalId = bvEvent.vueTarget && bvEvent.vueTarget.id;
|
||||
}
|
||||
|
||||
if (!modalId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalStack = this.$store.state.modalStack;
|
||||
|
||||
@@ -517,6 +525,7 @@ export default {
|
||||
|
||||
// Get previous modal
|
||||
const modalBefore = modalOnTop ? modalOnTop.prev : undefined;
|
||||
|
||||
if (modalBefore) this.$root.$emit('bv::show::modal', modalBefore, {fromRoot: true});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
.markdown {
|
||||
> p {
|
||||
margin-bottom: 0px;
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 14px;
|
||||
line-height: 1.17;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 4px;
|
||||
margin-top: 6px;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
|
||||
@@ -317,6 +317,8 @@ export default {
|
||||
methods: {
|
||||
async shown () {
|
||||
this.groups = await this.$store.dispatch('guilds:getMyGuilds');
|
||||
|
||||
if (this.user.party && this.user.party._id) {
|
||||
await this.$store.dispatch('party:getParty');
|
||||
const party = this.$store.state.party.data;
|
||||
if (party._id) {
|
||||
@@ -326,6 +328,7 @@ export default {
|
||||
privacy: 'private',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.groups.push({
|
||||
name: this.$t('publicChallengesTitle'),
|
||||
|
||||
@@ -11,7 +11,7 @@ div
|
||||
)
|
||||
| {{msg.user}}
|
||||
.svg-icon(v-html="tierIcon", v-if='showShowTierStyle')
|
||||
p.time {{msg.timestamp | timeAgo}}
|
||||
p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}}
|
||||
.text(v-markdown='msg.text')
|
||||
hr
|
||||
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
|
||||
@@ -72,6 +72,7 @@ div
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #878190;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.text {
|
||||
@@ -165,7 +166,7 @@ export default {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
date (value) {
|
||||
// @TODO: Add user preference
|
||||
// @TODO: Vue doesn't support this so we cant user preference
|
||||
return moment(value).toDate();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
:key="column",
|
||||
:taskListOverride='tasksByType[column]',
|
||||
v-on:editTask="editTask",
|
||||
v-on:loadGroupCompletedTodos="loadGroupCompletedTodos",
|
||||
:group='group',
|
||||
:searchText="searchText")
|
||||
</template>
|
||||
@@ -384,6 +385,20 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
async loadGroupCompletedTodos () {
|
||||
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
completedTodos.forEach((task) => {
|
||||
const existingTaskIndex = findIndex(this.tasksByType.todo, (todo) => {
|
||||
return todo._id === task._id;
|
||||
});
|
||||
if (existingTaskIndex === -1) {
|
||||
this.tasksByType.todo.push(task);
|
||||
}
|
||||
});
|
||||
},
|
||||
createTask (type) {
|
||||
this.taskFormPurpose = 'create';
|
||||
this.creatingTask = taskDefaults({type, text: ''});
|
||||
|
||||
@@ -19,19 +19,20 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
|
||||
p(v-once) {{$t('wantToJoinPartyDescription')}}
|
||||
button.btn.btn-primary(v-once, @click='shareUserId()') {{$t('shartUserId')}}
|
||||
.share-userid-options(v-if="shareUserIdShown")
|
||||
.option-item(v-once)
|
||||
.option-item(@click='copyUserId()')
|
||||
.svg-icon(v-html="icons.copy")
|
||||
input(type="text", v-model="user._id", id="userIdInput")
|
||||
| Copy User ID
|
||||
.option-item(v-once)
|
||||
//.option-item(v-once)
|
||||
.svg-icon(v-html="icons.greyBadge")
|
||||
| {{$t('lookingForGroup')}}
|
||||
.option-item(v-once)
|
||||
//.option-item(v-once)
|
||||
.svg-icon(v-html="icons.qrCode")
|
||||
| {{$t('qrCode')}}
|
||||
.option-item(v-once)
|
||||
//.option-item(v-once)
|
||||
.svg-icon.facebook(v-html="icons.facebook")
|
||||
| Facebook
|
||||
.option-item(v-once)
|
||||
//.option-item(v-once)
|
||||
.svg-icon(v-html="icons.twitter")
|
||||
| Twitter
|
||||
</template>
|
||||
@@ -108,10 +109,15 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
|
||||
border-radius: 2px;
|
||||
width: 220px;
|
||||
position: absolute;
|
||||
top: -8em;
|
||||
top: 9em;
|
||||
left: 4.8em;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
|
||||
#userIdInput {
|
||||
position: absolute;
|
||||
left: 1000rem;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
padding: 1em;
|
||||
|
||||
@@ -183,6 +189,12 @@ export default {
|
||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||
this.$router.push('/party');
|
||||
},
|
||||
copyUserId () {
|
||||
const copyText = document.getElementById('userIdInput');
|
||||
copyText.select();
|
||||
document.execCommand('copy');
|
||||
alert('User ID has been copied');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -76,10 +76,18 @@ router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }")
|
||||
|
||||
.gold {
|
||||
color: #fdbb5a;
|
||||
|
||||
.member-count {
|
||||
color: #fdbb5a;
|
||||
}
|
||||
}
|
||||
|
||||
.silver {
|
||||
color: #c2c2c2;
|
||||
|
||||
.member-count {
|
||||
color: #c2c2c2;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-column {
|
||||
|
||||
@@ -8,8 +8,8 @@ menu-dropdown.item-user(:right="true")
|
||||
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
||||
h3 {{ user.profile.name }}
|
||||
span.small-text {{ $t('editAvatar') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
|
||||
| {{ $t('messages') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated.d-flex.justify-content-between.align-items-center(@click.prevent='showInbox()')
|
||||
div {{ $t('messages') }}
|
||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages")
|
||||
a.dropdown-item(@click='showAvatar("backgrounds", "2018")') {{ $t('backgrounds') }}
|
||||
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
|
||||
|
||||
@@ -465,7 +465,7 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'won-challenge');
|
||||
break;
|
||||
case 'STREAK_ACHIEVEMENT':
|
||||
this.streak(this.user.achievements.streak);
|
||||
this.text(this.user.achievements.streak);
|
||||
this.playSound('Achievement_Unlocked');
|
||||
if (!this.user.preferences.suppressModals.streak) {
|
||||
this.$root.$emit('bv::show::modal', 'streak');
|
||||
@@ -485,7 +485,9 @@ export default {
|
||||
break;
|
||||
case 'CHALLENGE_JOINED_ACHIEVEMENT':
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.text(`${this.$t('achievement')}: ${this.$t('joinedChallenge')}`, () => {
|
||||
this.$root.$emit('bv::show::modal', 'joined-challenge');
|
||||
}, false);
|
||||
break;
|
||||
case 'INVITED_FRIEND_ACHIEVEMENT':
|
||||
this.playSound('Achievement_Unlocked');
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
h2.text-center Continue with Amazon
|
||||
#AmazonPayButton
|
||||
#AmazonPayWallet(v-if="amazonPayments.loggedIn", style="width: 400px; height: 228px;")
|
||||
#AmazonPayRecurring(v-if="amazonPayments.loggedIn && amazonPayments.type === 'subscription'",
|
||||
style="width: 400px; height: 140px;")
|
||||
template(v-if="amazonPayments.loggedIn && amazonPayments.type === 'subscription'")
|
||||
br
|
||||
p(v-html="$t('amazonPaymentsRecurring')")
|
||||
#AmazonPayRecurring(style="width: 400px; height: 140px;")
|
||||
.modal-footer
|
||||
.text-center
|
||||
button.btn.btn-primary(v-if="amazonPaymentsCanCheckout",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="pug">
|
||||
b-modal#send-gems(:title="title", :hide-footer="true", size='lg')
|
||||
b-modal#send-gems(:title="title", :hide-footer="true", size='lg', @hide='onHide()')
|
||||
.modal-body(v-if='userReceivingGems')
|
||||
.panel.panel-default(
|
||||
:class="gift.type === 'gems' ? 'panel-primary' : 'transparent'",
|
||||
@@ -138,6 +138,9 @@ export default {
|
||||
this.text(this.$t('sentGems'));
|
||||
this.close();
|
||||
},
|
||||
onHide () {
|
||||
this.gift.message = '';
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'send-gems');
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
transition(name="fade")
|
||||
.notification.callout.animated(:class="classes", v-if='show', @click='show = false')
|
||||
.notification.callout.animated(:class="classes", v-if='show', @click='handleOnClick()')
|
||||
.row(v-if='notification.type === "error"')
|
||||
.text.col-12
|
||||
div(v-html='notification.text')
|
||||
@@ -146,6 +146,15 @@ export default {
|
||||
beforeDestroy () {
|
||||
clearTimeout(this.timer);
|
||||
},
|
||||
methods: {
|
||||
handleOnClick () {
|
||||
if (typeof this.notification.onClick === 'function') {
|
||||
this.notification.onClick();
|
||||
}
|
||||
|
||||
this.show = false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show () {
|
||||
this.$store.dispatch('snackbars:remove', this.notification);
|
||||
|
||||
@@ -362,7 +362,7 @@ export default {
|
||||
type: this.type,
|
||||
filterType: this.activeFilter.label,
|
||||
}) :
|
||||
this.taskListOverride;
|
||||
this.filterByCompleted(this.taskListOverride, this.activeFilter.label);
|
||||
|
||||
let taggedList = this.filterByTagList(filteredTaskList, this.selectedTags);
|
||||
let searchedList = this.filterBySearchText(taggedList, this.searchText);
|
||||
@@ -556,8 +556,12 @@ export default {
|
||||
activateFilter (type, filter = '') {
|
||||
// Needs a separate API call as this data may not reside in store
|
||||
if (type === 'todo' && filter === 'complete2') {
|
||||
if (this.group && this.group._id) {
|
||||
this.$emit('loadGroupCompletedTodos');
|
||||
} else {
|
||||
this.loadCompletedTodos();
|
||||
}
|
||||
}
|
||||
|
||||
// the only time activateFilter is called with filter==='' is when the component is first created
|
||||
// this can be used to check If the user has set 'due' as default filter for daily
|
||||
@@ -594,6 +598,13 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
filterByCompleted (taskList, filter) {
|
||||
if (!taskList) return [];
|
||||
return taskList.filter(task => {
|
||||
if (filter === 'complete2') return task.completed;
|
||||
return !task.completed;
|
||||
});
|
||||
},
|
||||
filterByTagList (taskList, tagList = []) {
|
||||
let filteredTaskList = taskList;
|
||||
// filter requested tasks by tags
|
||||
|
||||
@@ -185,6 +185,15 @@
|
||||
: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")
|
||||
@@ -691,6 +700,7 @@ export default {
|
||||
calendar: calendarIcon,
|
||||
}),
|
||||
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
|
||||
sharedCompletion: 'recurringCompletion',
|
||||
members: [],
|
||||
memberNamesById: {},
|
||||
assignedMembers: [],
|
||||
@@ -811,6 +821,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';
|
||||
}
|
||||
|
||||
// @TODO: This whole component is mutating a prop and that causes issues. We need to not copy the prop similar to group modals
|
||||
@@ -892,10 +903,13 @@ export default {
|
||||
async submit () {
|
||||
if (this.newChecklistItem) this.addChecklistItem();
|
||||
|
||||
// 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;
|
||||
this.task.group.sharedCompletion = this.sharedCompletion;
|
||||
}
|
||||
|
||||
if (this.purpose === 'create') {
|
||||
|
||||
@@ -8,7 +8,7 @@ div
|
||||
button.btn.btn-secondary.gift-icon(@click='openSendGemsModal()', v-b-tooltip.hover.bottom="$t('sendGems')")
|
||||
.svg-icon.gift-icon(v-html="icons.gift")
|
||||
button.btn.btn-secondary.remove-icon(v-if='user._id !== this.userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) === -1',
|
||||
@click="blockUser()", v-b-tooltip.hover.right="$t('block')")
|
||||
@click="blockUser()", v-b-tooltip.hover.right="$t('blockWarning')")
|
||||
.svg-icon.remove-icon(v-html="icons.remove")
|
||||
button.btn.btn-secondary.positive-icon(v-if='user._id !== this.userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) !== -1',
|
||||
@click="unblockUser()", v-b-tooltip.hover.right="$t('unblock')")
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
.col-12.col-md-6
|
||||
h3(v-if='userLevel100Plus', v-once, v-html="$t('noMoreAllocate')")
|
||||
h3
|
||||
| {{$t('pointsAvailable')}}
|
||||
| {{$t('statPoints')}}
|
||||
.counter.badge(v-if='user.stats.points || userLevel100Plus')
|
||||
| {{user.stats.points}}
|
||||
.col-12.col-md-6
|
||||
@@ -135,16 +135,16 @@
|
||||
.row
|
||||
.col-12.col-md-3(v-for='(statInfo, stat) in allocateStatsList')
|
||||
.box.white.row.col-12
|
||||
.col-12.col-md-9
|
||||
.col-9
|
||||
div(:class='stat') {{ $t(stats[stat].title) }}
|
||||
.number {{ user.stats[stat] }}
|
||||
.points {{$t('pts')}}
|
||||
.col-12.col-md-3
|
||||
.col-3
|
||||
div
|
||||
.up(v-if='user.stats.points', @click='allocate(stat)')
|
||||
.up(v-if='showStatsSave', @click='allocate(stat)')
|
||||
div
|
||||
.down(@click='deallocate(stat)', v-if='user.stats.points')
|
||||
.row.save-row
|
||||
.down(v-if='showStatsSave', @click='deallocate(stat)')
|
||||
.row.save-row(v-if='showStatsSave')
|
||||
.col-12.col-md-6.offset-md-3.text-center
|
||||
button.btn.btn-primary(@click='saveAttributes()', :disabled='loading') {{ this.loading ? $t('loading') : $t('save') }}
|
||||
</template>
|
||||
@@ -238,6 +238,10 @@
|
||||
userLevel100Plus () {
|
||||
return this.user.stats.lvl >= 100;
|
||||
},
|
||||
showStatsSave () {
|
||||
const statsAreBeingUpdated = Object.values(this.statUpdates).find(stat => stat > 0);
|
||||
return Boolean(this.user.stats.points) || statsAreBeingUpdated;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getGearTitle (key) {
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
export default {
|
||||
methods: {
|
||||
makeGenericPurchase (item, type = 'buyModal', quantity = 1) {
|
||||
this.$store.dispatch('shops:genericPurchase', {
|
||||
async makeGenericPurchase (item, type = 'buyModal', quantity = 1) {
|
||||
try {
|
||||
await this.$store.dispatch('shops:genericPurchase', {
|
||||
pinType: item.pinType,
|
||||
type: item.purchaseType,
|
||||
key: item.key,
|
||||
currency: item.currency,
|
||||
quantity,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!e.request) {
|
||||
// axios request errors already handled by app.vue
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
text: e.message,
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
this.$root.$emit('playSound', 'Reward');
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ export default {
|
||||
streak (val) {
|
||||
this.notify(`${val}`, 'streak');
|
||||
},
|
||||
text (val, onClick) {
|
||||
text (val, onClick, timeout) {
|
||||
if (!val) return;
|
||||
this.notify(val, 'info', null, null, onClick);
|
||||
this.notify(val, 'info', null, null, onClick, timeout);
|
||||
},
|
||||
sign (number) {
|
||||
return getSign(number);
|
||||
@@ -66,14 +66,19 @@ export default {
|
||||
round (number, nDigits) {
|
||||
return round(number, nDigits);
|
||||
},
|
||||
notify (html, type, icon, sign) {
|
||||
notify (html, type, icon, sign, onClick, timeout) {
|
||||
if (typeof timeout === 'undefined') {
|
||||
timeout = true;
|
||||
}
|
||||
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
text: html,
|
||||
type,
|
||||
icon,
|
||||
sign,
|
||||
timeout: true,
|
||||
onClick,
|
||||
timeout,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -176,6 +176,11 @@ export async function getGroupTasks (store, payload) {
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getCompletedGroupTasks (store, payload) {
|
||||
let response = await axios.get(`/api/v4/tasks/group/${payload.groupId}?type=completedTodos`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function createGroupTasks (store, payload) {
|
||||
let response = await axios.post(`/api/v4/tasks/group/${payload.groupId}`, payload.tasks);
|
||||
return response.data.data;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"achievement": "Achievement",
|
||||
"share": "Share",
|
||||
"onwards": "Onwards!",
|
||||
"levelup": "By accomplishing your real life goals, you leveled up and are now fully healed!",
|
||||
|
||||
@@ -224,6 +224,6 @@
|
||||
"level": "Level",
|
||||
"allocated": "Allocated",
|
||||
"buffs": "Buffs",
|
||||
"pointsAvailable": "Points Available",
|
||||
"statPoints": "Stat Points",
|
||||
"pts": "pts"
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"PMDisabledCaptionText": "You can still send messages, but no one can send them to you.",
|
||||
"block": "Block",
|
||||
"unblock": "Un-block",
|
||||
"blockWarning": "Block - This will have no effect if the player is a moderator now or becomes a moderator in future.",
|
||||
"pm-reply": "Send a reply",
|
||||
"inbox": "Inbox",
|
||||
"messageRequired": "A message is required.",
|
||||
@@ -475,5 +476,9 @@
|
||||
"howToRequireApprovalDesc2": "Group Leaders and Managers can approve completed Tasks directly from the Task Board or from the Notifications panel.",
|
||||
"whatIsGroupManager": "What is a Group Manager?",
|
||||
"whatIsGroupManagerDesc": "A Group Manager is a user role that do not have access to the group's billing details, but can create, assign, and approve shared Tasks for the Group's members. Promote Group Managers from the Group’s member list.",
|
||||
"goToTaskBoard": "Go to Task Board"
|
||||
"goToTaskBoard": "Go to Task Board",
|
||||
"sharedCompletion": "Shared Completion",
|
||||
"recurringCompletion": "None - Group task does not complete",
|
||||
"singleCompletion": "Single - Completes when any assigned user finishes",
|
||||
"allAssignedCompletion": "All - Completes when all assigned users finish"
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
"mysticHourglassesTooltip": "Mystic Hourglasses",
|
||||
"paypal": "PayPal",
|
||||
"amazonPayments": "Amazon Payments",
|
||||
"amazonPaymentsRecurring": "Ticking the checkbox below is necessary for your subscription to be created. It allows your Amazon account to be used for ongoing payments for <strong>this</strong> subscription. It will not cause your Amazon account to be automatically used for any future purchases.",
|
||||
"timezone": "Time Zone",
|
||||
"timezoneUTC": "Habitica uses the time zone set on your PC, which is: <strong><%= utc %></strong>",
|
||||
"timezoneInfo": "If that time zone is wrong, first reload this page using your browser's reload or refresh button to ensure that Habitica has the most recent information. If it is still wrong, adjust the time zone on your PC and then reload this page again.<br><br> <strong>If you use Habitica on other PCs or mobile devices, the time zone must be the same on them all.</strong> If your Dailies have been resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices.",
|
||||
|
||||
@@ -449,7 +449,7 @@ api.getGroupChallenges = {
|
||||
method: 'GET',
|
||||
url: '/challenges/groups/:groupId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
userFieldsToInclude: ['_id', 'party', 'guilds'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -463,10 +463,10 @@ api.getGroupChallenges = {
|
||||
if (groupId === 'party') groupId = user.party._id;
|
||||
if (groupId === 'habitrpg') groupId = TAVERN_ID;
|
||||
|
||||
let group = await Group.getGroup({user, groupId});
|
||||
const group = await Group.getGroup({ user, groupId });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let challenges = await Challenge.find({group: groupId})
|
||||
const challenges = await Challenge.find({ group: groupId })
|
||||
.sort('-createdAt')
|
||||
// .populate('leader', nameFields) // Only populate the leader as the group is implicit
|
||||
.exec();
|
||||
|
||||
@@ -384,7 +384,7 @@ api.getGroup = {
|
||||
method: 'GET',
|
||||
url: '/groups/:groupId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
userFieldsToInclude: ['_id', 'party', 'guilds', 'contributor'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '../../libs/webhook';
|
||||
import { removeFromArray } from '../../libs/collectionManipulators';
|
||||
import * as Tasks from '../../models/task';
|
||||
import { handleSharedCompletion } from '../../libs/groupTasks';
|
||||
import { model as Challenge } from '../../models/challenge';
|
||||
import { model as Group } from '../../models/group';
|
||||
import { model as User } from '../../models/user';
|
||||
@@ -287,7 +288,7 @@ api.getUserTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/user',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
userFieldsToInclude: ['_id', 'tasksOrder', 'preferences'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
@@ -297,10 +298,10 @@ api.getUserTasks = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
let dueDate = req.query.dueDate;
|
||||
const user = res.locals.user;
|
||||
const dueDate = req.query.dueDate;
|
||||
|
||||
let tasks = await getTasks(req, res, {user, dueDate});
|
||||
const tasks = await getTasks(req, res, { user, dueDate });
|
||||
return res.respond(200, tasks);
|
||||
},
|
||||
};
|
||||
@@ -490,6 +491,9 @@ api.updateTask = {
|
||||
if (sanitizedObj.requiresApproval) {
|
||||
task.group.approval.required = true;
|
||||
}
|
||||
if (sanitizedObj.sharedCompletion) {
|
||||
task.group.sharedCompletion = sanitizedObj.sharedCompletion;
|
||||
}
|
||||
|
||||
setNextDue(task, user);
|
||||
let savedTask = await task.save();
|
||||
@@ -653,6 +657,12 @@ api.scoreTask = {
|
||||
user.save(),
|
||||
task.save(),
|
||||
];
|
||||
|
||||
if (task.group && task.group.taskId) {
|
||||
await handleSharedCompletion(task);
|
||||
}
|
||||
|
||||
// Save results and handle request
|
||||
if (taskOrderPromise) promises.push(taskOrderPromise);
|
||||
let results = await Promise.all(promises);
|
||||
|
||||
|
||||
@@ -12,10 +12,13 @@ import {
|
||||
getTasks,
|
||||
moveTask,
|
||||
} from '../../../libs/taskManager';
|
||||
import { handleSharedCompletion } from '../../../libs/groupTasks';
|
||||
import apiError from '../../../libs/apiError';
|
||||
|
||||
let requiredGroupFields = '_id leader tasksOrder name';
|
||||
// @TODO: abstract to task lib
|
||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future
|
||||
|
||||
function canNotEditTasks (group, user, assignedUserId) {
|
||||
let isNotGroupLeader = group.leader !== user._id;
|
||||
@@ -338,7 +341,7 @@ api.approveTask = {
|
||||
}
|
||||
|
||||
// Remove old notifications
|
||||
let managerPromises = [];
|
||||
let approvalPromises = [];
|
||||
managers.forEach((manager) => {
|
||||
let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
|
||||
return notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
|
||||
@@ -346,7 +349,7 @@ api.approveTask = {
|
||||
|
||||
if (notificationIndex !== -1) {
|
||||
manager.notifications.splice(notificationIndex, 1);
|
||||
managerPromises.push(manager.save());
|
||||
approvalPromises.push(manager.save());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -362,9 +365,11 @@ api.approveTask = {
|
||||
direction,
|
||||
});
|
||||
|
||||
managerPromises.push(task.save());
|
||||
managerPromises.push(assignedUser.save());
|
||||
await Promise.all(managerPromises);
|
||||
await handleSharedCompletion(task);
|
||||
|
||||
approvalPromises.push(task.save());
|
||||
approvalPromises.push(assignedUser.save());
|
||||
await Promise.all(approvalPromises);
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
|
||||
61
website/server/libs/groupTasks.js
Normal file
61
website/server/libs/groupTasks.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as Tasks from '../models/task';
|
||||
|
||||
const SHARED_COMPLETION = {
|
||||
default: 'recurringCompletion',
|
||||
single: 'singleCompletion',
|
||||
every: 'allAssignedCompletion',
|
||||
};
|
||||
|
||||
async function _completeMasterTask (masterTask) {
|
||||
masterTask.completed = true;
|
||||
await masterTask.save();
|
||||
}
|
||||
|
||||
async function _deleteUnfinishedTasks (groupMemberTask) {
|
||||
await Tasks.Task.deleteMany({
|
||||
'group.taskId': groupMemberTask.group.taskId,
|
||||
$and: [
|
||||
{userId: {$exists: true}},
|
||||
{userId: {$ne: groupMemberTask.userId}},
|
||||
],
|
||||
}).exec();
|
||||
}
|
||||
|
||||
async function _evaluateAllAssignedCompletion (masterTask) {
|
||||
let completions;
|
||||
if (masterTask.group.approval && masterTask.group.approval.required) {
|
||||
completions = await Tasks.Task.count({
|
||||
'group.taskId': masterTask._id,
|
||||
'group.approval.approved': true,
|
||||
}).exec();
|
||||
completions++;
|
||||
} else {
|
||||
completions = await Tasks.Task.count({
|
||||
'group.taskId': masterTask._id,
|
||||
completed: true,
|
||||
}).exec();
|
||||
}
|
||||
if (completions >= masterTask.group.assignedUsers.length) {
|
||||
await _completeMasterTask(masterTask);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSharedCompletion (groupMemberTask) {
|
||||
let masterTask = await Tasks.Task.findOne({
|
||||
_id: groupMemberTask.group.taskId,
|
||||
}).exec();
|
||||
|
||||
if (!masterTask || !masterTask.group || masterTask.type !== 'todo') return;
|
||||
|
||||
if (masterTask.group.sharedCompletion === SHARED_COMPLETION.single) {
|
||||
await _deleteUnfinishedTasks(groupMemberTask);
|
||||
await _completeMasterTask(masterTask);
|
||||
} else if (masterTask.group.sharedCompletion === SHARED_COMPLETION.every) {
|
||||
await _evaluateAllAssignedCompletion(masterTask);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
SHARED_COMPLETION,
|
||||
handleSharedCompletion,
|
||||
};
|
||||
@@ -3,6 +3,9 @@ import * as Tasks from '../models/task';
|
||||
import {
|
||||
BadRequest,
|
||||
} from './errors';
|
||||
import {
|
||||
SHARED_COMPLETION,
|
||||
} from './groupTasks';
|
||||
import _ from 'lodash';
|
||||
import shared from '../../common';
|
||||
|
||||
@@ -96,6 +99,7 @@ export async function createTasks (req, res, options = {}) {
|
||||
if (taskData.requiresApproval) {
|
||||
newTask.group.approval.required = true;
|
||||
}
|
||||
newTask.group.sharedCompletion = taskData.sharedCompletion || SHARED_COMPLETION.default;
|
||||
} else {
|
||||
newTask.userId = user._id;
|
||||
}
|
||||
@@ -183,11 +187,12 @@ export async function getTasks (req, res, options = {}) {
|
||||
limit = 0; // no limit
|
||||
}
|
||||
|
||||
query = {
|
||||
userId: user._id,
|
||||
type: 'todo',
|
||||
completed: true,
|
||||
};
|
||||
query.type = 'todo';
|
||||
query.completed = true;
|
||||
|
||||
if (owner._id === user._id) {
|
||||
query.userId = user._id;
|
||||
}
|
||||
|
||||
sort = {
|
||||
dateCompleted: -1,
|
||||
|
||||
@@ -9,15 +9,19 @@ import url from 'url';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
||||
|
||||
function getUserFields (userFieldsToExclude, req) {
|
||||
function getUserFields (options, req) {
|
||||
// A list of user fields that aren't needed for the route and are not loaded from the db.
|
||||
// Must be an array
|
||||
if (userFieldsToExclude) {
|
||||
return userFieldsToExclude.map(field => {
|
||||
if (options.userFieldsToExclude) {
|
||||
return options.userFieldsToExclude.map(field => {
|
||||
return `-${field}`; // -${field} means exclude ${field} in mongodb
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
if (options.userFieldsToInclude) {
|
||||
return options.userFieldsToInclude.join(' ');
|
||||
}
|
||||
|
||||
// Allows GET requests to /user to specify a list of user fields to return instead of the entire doc
|
||||
// Notifications are always included
|
||||
const urlPath = url.parse(req.url).pathname;
|
||||
@@ -50,7 +54,7 @@ export function authWithHeaders (options = {}) {
|
||||
apiToken,
|
||||
};
|
||||
|
||||
const fields = getUserFields(options.userFieldsToExclude, req);
|
||||
const fields = getUserFields(options, req);
|
||||
const findPromise = fields ? User.findOne(userQuery).select(fields) : User.findOne(userQuery);
|
||||
|
||||
return findPromise
|
||||
|
||||
@@ -1319,6 +1319,7 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
|
||||
|
||||
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
|
||||
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
|
||||
updateCmd.$set['group.sharedCompletion'] = taskToSync.group.sharedCompletion;
|
||||
|
||||
let taskSchema = Tasks[taskToSync.type];
|
||||
|
||||
@@ -1414,6 +1415,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
||||
|
||||
matchingTask.group.approval.required = taskToSync.group.approval.required;
|
||||
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
|
||||
matchingTask.group.sharedCompletion = taskToSync.group.sharedCompletion;
|
||||
|
||||
// sync checklist
|
||||
if (taskToSync.checklist) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import baseModel from '../libs/baseModel';
|
||||
import { InternalServerError } from '../libs/errors';
|
||||
import _ from 'lodash';
|
||||
import { preenHistory } from '../libs/preening';
|
||||
import { SHARED_COMPLETION } from '../libs/groupTasks';
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
@@ -111,6 +112,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},
|
||||
},
|
||||
|
||||
reminders: [{
|
||||
|
||||
Reference in New Issue
Block a user