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'),
|
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 memberTasks = await member.get('/tasks/user');
|
||||||
let syncedTask = find(memberTasks, findAssignedTask);
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|
||||||
@@ -137,4 +137,112 @@ describe('POST /tasks/:id/score/:direction', () => {
|
|||||||
expect(updatedTask.completed).to.equal(true);
|
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
|
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';
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
/* @TODO: The modal-open class is not being removed. Let's try this for now */
|
/* @TODO: The modal-open class is not being removed. Let's try this for now */
|
||||||
.modal, .modal-open {
|
.modal {
|
||||||
overflow-y: scroll !important;
|
overflow-y: scroll !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,8 +499,16 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.$root.$on('bv::modal::hidden', (bvEvent) => {
|
this.$root.$on('bv::modal::hidden', (bvEvent) => {
|
||||||
const modalId = bvEvent.target && bvEvent.target.id;
|
let modalId = bvEvent.target && bvEvent.target.id;
|
||||||
if (!modalId) return;
|
|
||||||
|
// 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;
|
const modalStack = this.$store.state.modalStack;
|
||||||
|
|
||||||
@@ -517,6 +525,7 @@ export default {
|
|||||||
|
|
||||||
// Get previous modal
|
// Get previous modal
|
||||||
const modalBefore = modalOnTop ? modalOnTop.prev : undefined;
|
const modalBefore = modalOnTop ? modalOnTop.prev : undefined;
|
||||||
|
|
||||||
if (modalBefore) this.$root.$emit('bv::show::modal', modalBefore, {fromRoot: true});
|
if (modalBefore) this.$root.$emit('bv::show::modal', modalBefore, {fromRoot: true});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
.markdown {
|
.markdown {
|
||||||
> p {
|
p {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
line-height: 1.17;
|
line-height: 1.17;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
color: $gray-10;
|
color: $gray-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -317,14 +317,17 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async shown () {
|
async shown () {
|
||||||
this.groups = await this.$store.dispatch('guilds:getMyGuilds');
|
this.groups = await this.$store.dispatch('guilds:getMyGuilds');
|
||||||
await this.$store.dispatch('party:getParty');
|
|
||||||
const party = this.$store.state.party.data;
|
if (this.user.party && this.user.party._id) {
|
||||||
if (party._id) {
|
await this.$store.dispatch('party:getParty');
|
||||||
this.groups.push({
|
const party = this.$store.state.party.data;
|
||||||
name: party.name,
|
if (party._id) {
|
||||||
_id: party._id,
|
this.groups.push({
|
||||||
privacy: 'private',
|
name: party.name,
|
||||||
});
|
_id: party._id,
|
||||||
|
privacy: 'private',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.groups.push({
|
this.groups.push({
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ div
|
|||||||
)
|
)
|
||||||
| {{msg.user}}
|
| {{msg.user}}
|
||||||
.svg-icon(v-html="tierIcon", v-if='showShowTierStyle')
|
.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')
|
.text(v-markdown='msg.text')
|
||||||
hr
|
hr
|
||||||
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
|
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
|
||||||
@@ -72,6 +72,7 @@ div
|
|||||||
.time {
|
.time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #878190;
|
color: #878190;
|
||||||
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
@@ -165,7 +166,7 @@ export default {
|
|||||||
return moment(value).fromNow();
|
return moment(value).fromNow();
|
||||||
},
|
},
|
||||||
date (value) {
|
date (value) {
|
||||||
// @TODO: Add user preference
|
// @TODO: Vue doesn't support this so we cant user preference
|
||||||
return moment(value).toDate();
|
return moment(value).toDate();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
:key="column",
|
:key="column",
|
||||||
:taskListOverride='tasksByType[column]',
|
:taskListOverride='tasksByType[column]',
|
||||||
v-on:editTask="editTask",
|
v-on:editTask="editTask",
|
||||||
|
v-on:loadGroupCompletedTodos="loadGroupCompletedTodos",
|
||||||
:group='group',
|
:group='group',
|
||||||
:searchText="searchText")
|
:searchText="searchText")
|
||||||
</template>
|
</template>
|
||||||
@@ -384,6 +385,20 @@ export default {
|
|||||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
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) {
|
createTask (type) {
|
||||||
this.taskFormPurpose = 'create';
|
this.taskFormPurpose = 'create';
|
||||||
this.creatingTask = taskDefaults({type, text: ''});
|
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')}}
|
p(v-once) {{$t('wantToJoinPartyDescription')}}
|
||||||
button.btn.btn-primary(v-once, @click='shareUserId()') {{$t('shartUserId')}}
|
button.btn.btn-primary(v-once, @click='shareUserId()') {{$t('shartUserId')}}
|
||||||
.share-userid-options(v-if="shareUserIdShown")
|
.share-userid-options(v-if="shareUserIdShown")
|
||||||
.option-item(v-once)
|
.option-item(@click='copyUserId()')
|
||||||
.svg-icon(v-html="icons.copy")
|
.svg-icon(v-html="icons.copy")
|
||||||
|
input(type="text", v-model="user._id", id="userIdInput")
|
||||||
| Copy User ID
|
| Copy User ID
|
||||||
.option-item(v-once)
|
//.option-item(v-once)
|
||||||
.svg-icon(v-html="icons.greyBadge")
|
.svg-icon(v-html="icons.greyBadge")
|
||||||
| {{$t('lookingForGroup')}}
|
| {{$t('lookingForGroup')}}
|
||||||
.option-item(v-once)
|
//.option-item(v-once)
|
||||||
.svg-icon(v-html="icons.qrCode")
|
.svg-icon(v-html="icons.qrCode")
|
||||||
| {{$t('qrCode')}}
|
| {{$t('qrCode')}}
|
||||||
.option-item(v-once)
|
//.option-item(v-once)
|
||||||
.svg-icon.facebook(v-html="icons.facebook")
|
.svg-icon.facebook(v-html="icons.facebook")
|
||||||
| Facebook
|
| Facebook
|
||||||
.option-item(v-once)
|
//.option-item(v-once)
|
||||||
.svg-icon(v-html="icons.twitter")
|
.svg-icon(v-html="icons.twitter")
|
||||||
| Twitter
|
| Twitter
|
||||||
</template>
|
</template>
|
||||||
@@ -108,10 +109,15 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -8em;
|
top: 9em;
|
||||||
left: 4.8em;
|
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);
|
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 {
|
.option-item {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
||||||
@@ -183,6 +189,12 @@ export default {
|
|||||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||||
this.$router.push('/party');
|
this.$router.push('/party');
|
||||||
},
|
},
|
||||||
|
copyUserId () {
|
||||||
|
const copyText = document.getElementById('userIdInput');
|
||||||
|
copyText.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert('User ID has been copied');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -76,10 +76,18 @@ router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }")
|
|||||||
|
|
||||||
.gold {
|
.gold {
|
||||||
color: #fdbb5a;
|
color: #fdbb5a;
|
||||||
|
|
||||||
|
.member-count {
|
||||||
|
color: #fdbb5a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.silver {
|
.silver {
|
||||||
color: #c2c2c2;
|
color: #c2c2c2;
|
||||||
|
|
||||||
|
.member-count {
|
||||||
|
color: #c2c2c2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-column {
|
.badge-column {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ menu-dropdown.item-user(:right="true")
|
|||||||
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
||||||
h3 {{ user.profile.name }}
|
h3 {{ user.profile.name }}
|
||||||
span.small-text {{ $t('editAvatar') }}
|
span.small-text {{ $t('editAvatar') }}
|
||||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
|
a.nav-link.dropdown-item.dropdown-separated.d-flex.justify-content-between.align-items-center(@click.prevent='showInbox()')
|
||||||
| {{ $t('messages') }}
|
div {{ $t('messages') }}
|
||||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages")
|
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='showAvatar("backgrounds", "2018")') {{ $t('backgrounds') }}
|
||||||
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
|
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ export default {
|
|||||||
this.$root.$emit('bv::show::modal', 'won-challenge');
|
this.$root.$emit('bv::show::modal', 'won-challenge');
|
||||||
break;
|
break;
|
||||||
case 'STREAK_ACHIEVEMENT':
|
case 'STREAK_ACHIEVEMENT':
|
||||||
this.streak(this.user.achievements.streak);
|
this.text(this.user.achievements.streak);
|
||||||
this.playSound('Achievement_Unlocked');
|
this.playSound('Achievement_Unlocked');
|
||||||
if (!this.user.preferences.suppressModals.streak) {
|
if (!this.user.preferences.suppressModals.streak) {
|
||||||
this.$root.$emit('bv::show::modal', 'streak');
|
this.$root.$emit('bv::show::modal', 'streak');
|
||||||
@@ -485,7 +485,9 @@ export default {
|
|||||||
break;
|
break;
|
||||||
case 'CHALLENGE_JOINED_ACHIEVEMENT':
|
case 'CHALLENGE_JOINED_ACHIEVEMENT':
|
||||||
this.playSound('Achievement_Unlocked');
|
this.playSound('Achievement_Unlocked');
|
||||||
this.$root.$emit('bv::show::modal', 'joined-challenge');
|
this.text(`${this.$t('achievement')}: ${this.$t('joinedChallenge')}`, () => {
|
||||||
|
this.$root.$emit('bv::show::modal', 'joined-challenge');
|
||||||
|
}, false);
|
||||||
break;
|
break;
|
||||||
case 'INVITED_FRIEND_ACHIEVEMENT':
|
case 'INVITED_FRIEND_ACHIEVEMENT':
|
||||||
this.playSound('Achievement_Unlocked');
|
this.playSound('Achievement_Unlocked');
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
h2.text-center Continue with Amazon
|
h2.text-center Continue with Amazon
|
||||||
#AmazonPayButton
|
#AmazonPayButton
|
||||||
#AmazonPayWallet(v-if="amazonPayments.loggedIn", style="width: 400px; height: 228px;")
|
#AmazonPayWallet(v-if="amazonPayments.loggedIn", style="width: 400px; height: 228px;")
|
||||||
#AmazonPayRecurring(v-if="amazonPayments.loggedIn && amazonPayments.type === 'subscription'",
|
template(v-if="amazonPayments.loggedIn && amazonPayments.type === 'subscription'")
|
||||||
style="width: 400px; height: 140px;")
|
br
|
||||||
|
p(v-html="$t('amazonPaymentsRecurring')")
|
||||||
|
#AmazonPayRecurring(style="width: 400px; height: 140px;")
|
||||||
.modal-footer
|
.modal-footer
|
||||||
.text-center
|
.text-center
|
||||||
button.btn.btn-primary(v-if="amazonPaymentsCanCheckout",
|
button.btn.btn-primary(v-if="amazonPaymentsCanCheckout",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template lang="pug">
|
<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')
|
.modal-body(v-if='userReceivingGems')
|
||||||
.panel.panel-default(
|
.panel.panel-default(
|
||||||
:class="gift.type === 'gems' ? 'panel-primary' : 'transparent'",
|
:class="gift.type === 'gems' ? 'panel-primary' : 'transparent'",
|
||||||
@@ -138,6 +138,9 @@ export default {
|
|||||||
this.text(this.$t('sentGems'));
|
this.text(this.$t('sentGems'));
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
|
onHide () {
|
||||||
|
this.gift.message = '';
|
||||||
|
},
|
||||||
close () {
|
close () {
|
||||||
this.$root.$emit('bv::hide::modal', 'send-gems');
|
this.$root.$emit('bv::hide::modal', 'send-gems');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
transition(name="fade")
|
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"')
|
.row(v-if='notification.type === "error"')
|
||||||
.text.col-12
|
.text.col-12
|
||||||
div(v-html='notification.text')
|
div(v-html='notification.text')
|
||||||
@@ -146,6 +146,15 @@ export default {
|
|||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
handleOnClick () {
|
||||||
|
if (typeof this.notification.onClick === 'function') {
|
||||||
|
this.notification.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.show = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
show () {
|
show () {
|
||||||
this.$store.dispatch('snackbars:remove', this.notification);
|
this.$store.dispatch('snackbars:remove', this.notification);
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ export default {
|
|||||||
type: this.type,
|
type: this.type,
|
||||||
filterType: this.activeFilter.label,
|
filterType: this.activeFilter.label,
|
||||||
}) :
|
}) :
|
||||||
this.taskListOverride;
|
this.filterByCompleted(this.taskListOverride, this.activeFilter.label);
|
||||||
|
|
||||||
let taggedList = this.filterByTagList(filteredTaskList, this.selectedTags);
|
let taggedList = this.filterByTagList(filteredTaskList, this.selectedTags);
|
||||||
let searchedList = this.filterBySearchText(taggedList, this.searchText);
|
let searchedList = this.filterBySearchText(taggedList, this.searchText);
|
||||||
@@ -556,7 +556,11 @@ export default {
|
|||||||
activateFilter (type, filter = '') {
|
activateFilter (type, filter = '') {
|
||||||
// Needs a separate API call as this data may not reside in store
|
// Needs a separate API call as this data may not reside in store
|
||||||
if (type === 'todo' && filter === 'complete2') {
|
if (type === 'todo' && filter === 'complete2') {
|
||||||
this.loadCompletedTodos();
|
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
|
// the only time activateFilter is called with filter==='' is when the component is first created
|
||||||
@@ -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 = []) {
|
filterByTagList (taskList, tagList = []) {
|
||||||
let filteredTaskList = taskList;
|
let filteredTaskList = taskList;
|
||||||
// filter requested tasks by tags
|
// filter requested tasks by tags
|
||||||
|
|||||||
@@ -185,6 +185,15 @@
|
|||||||
:checked="requiresApproval",
|
:checked="requiresApproval",
|
||||||
@change="updateRequiresApproval"
|
@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(v-if="task.type !== 'reward'")
|
||||||
.advanced-settings-toggle.d-flex.justify-content-between.align-items-center(@click = "showAdvancedOptions = !showAdvancedOptions")
|
.advanced-settings-toggle.d-flex.justify-content-between.align-items-center(@click = "showAdvancedOptions = !showAdvancedOptions")
|
||||||
@@ -691,6 +700,7 @@ export default {
|
|||||||
calendar: calendarIcon,
|
calendar: calendarIcon,
|
||||||
}),
|
}),
|
||||||
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
|
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
|
||||||
|
sharedCompletion: 'recurringCompletion',
|
||||||
members: [],
|
members: [],
|
||||||
memberNamesById: {},
|
memberNamesById: {},
|
||||||
assignedMembers: [],
|
assignedMembers: [],
|
||||||
@@ -811,6 +821,7 @@ export default {
|
|||||||
});
|
});
|
||||||
this.assignedMembers = [];
|
this.assignedMembers = [];
|
||||||
if (this.task.group && this.task.group.assignedUsers) this.assignedMembers = this.task.group.assignedUsers;
|
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
|
// @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 () {
|
async submit () {
|
||||||
if (this.newChecklistItem) this.addChecklistItem();
|
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) {
|
if (this.groupId) {
|
||||||
this.task.group.assignedUsers = this.assignedMembers;
|
this.task.group.assignedUsers = this.assignedMembers;
|
||||||
this.task.requiresApproval = this.requiresApproval;
|
this.task.requiresApproval = this.requiresApproval;
|
||||||
this.task.group.approval.required = this.requiresApproval;
|
this.task.group.approval.required = this.requiresApproval;
|
||||||
|
this.task.sharedCompletion = this.sharedCompletion;
|
||||||
|
this.task.group.sharedCompletion = this.sharedCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.purpose === 'create') {
|
if (this.purpose === 'create') {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ div
|
|||||||
button.btn.btn-secondary.gift-icon(@click='openSendGemsModal()', v-b-tooltip.hover.bottom="$t('sendGems')")
|
button.btn.btn-secondary.gift-icon(@click='openSendGemsModal()', v-b-tooltip.hover.bottom="$t('sendGems')")
|
||||||
.svg-icon.gift-icon(v-html="icons.gift")
|
.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',
|
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")
|
.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',
|
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')")
|
@click="unblockUser()", v-b-tooltip.hover.right="$t('unblock')")
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
.col-12.col-md-6
|
.col-12.col-md-6
|
||||||
h3(v-if='userLevel100Plus', v-once, v-html="$t('noMoreAllocate')")
|
h3(v-if='userLevel100Plus', v-once, v-html="$t('noMoreAllocate')")
|
||||||
h3
|
h3
|
||||||
| {{$t('pointsAvailable')}}
|
| {{$t('statPoints')}}
|
||||||
.counter.badge(v-if='user.stats.points || userLevel100Plus')
|
.counter.badge(v-if='user.stats.points || userLevel100Plus')
|
||||||
| {{user.stats.points}}
|
| {{user.stats.points}}
|
||||||
.col-12.col-md-6
|
.col-12.col-md-6
|
||||||
@@ -135,16 +135,16 @@
|
|||||||
.row
|
.row
|
||||||
.col-12.col-md-3(v-for='(statInfo, stat) in allocateStatsList')
|
.col-12.col-md-3(v-for='(statInfo, stat) in allocateStatsList')
|
||||||
.box.white.row.col-12
|
.box.white.row.col-12
|
||||||
.col-12.col-md-9
|
.col-9
|
||||||
div(:class='stat') {{ $t(stats[stat].title) }}
|
div(:class='stat') {{ $t(stats[stat].title) }}
|
||||||
.number {{ user.stats[stat] }}
|
.number {{ user.stats[stat] }}
|
||||||
.points {{$t('pts')}}
|
.points {{$t('pts')}}
|
||||||
.col-12.col-md-3
|
.col-3
|
||||||
div
|
div
|
||||||
.up(v-if='user.stats.points', @click='allocate(stat)')
|
.up(v-if='showStatsSave', @click='allocate(stat)')
|
||||||
div
|
div
|
||||||
.down(@click='deallocate(stat)', v-if='user.stats.points')
|
.down(v-if='showStatsSave', @click='deallocate(stat)')
|
||||||
.row.save-row
|
.row.save-row(v-if='showStatsSave')
|
||||||
.col-12.col-md-6.offset-md-3.text-center
|
.col-12.col-md-6.offset-md-3.text-center
|
||||||
button.btn.btn-primary(@click='saveAttributes()', :disabled='loading') {{ this.loading ? $t('loading') : $t('save') }}
|
button.btn.btn-primary(@click='saveAttributes()', :disabled='loading') {{ this.loading ? $t('loading') : $t('save') }}
|
||||||
</template>
|
</template>
|
||||||
@@ -238,6 +238,10 @@
|
|||||||
userLevel100Plus () {
|
userLevel100Plus () {
|
||||||
return this.user.stats.lvl >= 100;
|
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: {
|
methods: {
|
||||||
getGearTitle (key) {
|
getGearTitle (key) {
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
makeGenericPurchase (item, type = 'buyModal', quantity = 1) {
|
async makeGenericPurchase (item, type = 'buyModal', quantity = 1) {
|
||||||
this.$store.dispatch('shops:genericPurchase', {
|
try {
|
||||||
pinType: item.pinType,
|
await this.$store.dispatch('shops:genericPurchase', {
|
||||||
type: item.purchaseType,
|
pinType: item.pinType,
|
||||||
key: item.key,
|
type: item.purchaseType,
|
||||||
currency: item.currency,
|
key: item.key,
|
||||||
quantity,
|
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');
|
this.$root.$emit('playSound', 'Reward');
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ export default {
|
|||||||
streak (val) {
|
streak (val) {
|
||||||
this.notify(`${val}`, 'streak');
|
this.notify(`${val}`, 'streak');
|
||||||
},
|
},
|
||||||
text (val, onClick) {
|
text (val, onClick, timeout) {
|
||||||
if (!val) return;
|
if (!val) return;
|
||||||
this.notify(val, 'info', null, null, onClick);
|
this.notify(val, 'info', null, null, onClick, timeout);
|
||||||
},
|
},
|
||||||
sign (number) {
|
sign (number) {
|
||||||
return getSign(number);
|
return getSign(number);
|
||||||
@@ -66,14 +66,19 @@ export default {
|
|||||||
round (number, nDigits) {
|
round (number, nDigits) {
|
||||||
return 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', {
|
this.$store.dispatch('snackbars:add', {
|
||||||
title: '',
|
title: '',
|
||||||
text: html,
|
text: html,
|
||||||
type,
|
type,
|
||||||
icon,
|
icon,
|
||||||
sign,
|
sign,
|
||||||
timeout: true,
|
onClick,
|
||||||
|
timeout,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ export async function getGroupTasks (store, payload) {
|
|||||||
return response.data.data;
|
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) {
|
export async function createGroupTasks (store, payload) {
|
||||||
let response = await axios.post(`/api/v4/tasks/group/${payload.groupId}`, payload.tasks);
|
let response = await axios.post(`/api/v4/tasks/group/${payload.groupId}`, payload.tasks);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"achievement": "Achievement",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"onwards": "Onwards!",
|
"onwards": "Onwards!",
|
||||||
"levelup": "By accomplishing your real life goals, you leveled up and are now fully healed!",
|
"levelup": "By accomplishing your real life goals, you leveled up and are now fully healed!",
|
||||||
|
|||||||
@@ -224,6 +224,6 @@
|
|||||||
"level": "Level",
|
"level": "Level",
|
||||||
"allocated": "Allocated",
|
"allocated": "Allocated",
|
||||||
"buffs": "Buffs",
|
"buffs": "Buffs",
|
||||||
"pointsAvailable": "Points Available",
|
"statPoints": "Stat Points",
|
||||||
"pts": "pts"
|
"pts": "pts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@
|
|||||||
"PMDisabledCaptionText": "You can still send messages, but no one can send them to you.",
|
"PMDisabledCaptionText": "You can still send messages, but no one can send them to you.",
|
||||||
"block": "Block",
|
"block": "Block",
|
||||||
"unblock": "Un-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",
|
"pm-reply": "Send a reply",
|
||||||
"inbox": "Inbox",
|
"inbox": "Inbox",
|
||||||
"messageRequired": "A message is required.",
|
"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.",
|
"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?",
|
"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.",
|
"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",
|
"mysticHourglassesTooltip": "Mystic Hourglasses",
|
||||||
"paypal": "PayPal",
|
"paypal": "PayPal",
|
||||||
"amazonPayments": "Amazon Payments",
|
"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",
|
"timezone": "Time Zone",
|
||||||
"timezoneUTC": "Habitica uses the time zone set on your PC, which is: <strong><%= utc %></strong>",
|
"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.",
|
"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',
|
method: 'GET',
|
||||||
url: '/challenges/groups/:groupId',
|
url: '/challenges/groups/:groupId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders({
|
||||||
userFieldsToExclude: ['inbox'],
|
userFieldsToInclude: ['_id', 'party', 'guilds'],
|
||||||
})],
|
})],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -463,10 +463,10 @@ api.getGroupChallenges = {
|
|||||||
if (groupId === 'party') groupId = user.party._id;
|
if (groupId === 'party') groupId = user.party._id;
|
||||||
if (groupId === 'habitrpg') groupId = TAVERN_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'));
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||||
|
|
||||||
let challenges = await Challenge.find({group: groupId})
|
const challenges = await Challenge.find({ group: groupId })
|
||||||
.sort('-createdAt')
|
.sort('-createdAt')
|
||||||
// .populate('leader', nameFields) // Only populate the leader as the group is implicit
|
// .populate('leader', nameFields) // Only populate the leader as the group is implicit
|
||||||
.exec();
|
.exec();
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ api.getGroup = {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/groups/:groupId',
|
url: '/groups/:groupId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders({
|
||||||
userFieldsToExclude: ['inbox'],
|
userFieldsToInclude: ['_id', 'party', 'guilds', 'contributor'],
|
||||||
})],
|
})],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from '../../libs/webhook';
|
} from '../../libs/webhook';
|
||||||
import { removeFromArray } from '../../libs/collectionManipulators';
|
import { removeFromArray } from '../../libs/collectionManipulators';
|
||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
|
import { handleSharedCompletion } from '../../libs/groupTasks';
|
||||||
import { model as Challenge } from '../../models/challenge';
|
import { model as Challenge } from '../../models/challenge';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
@@ -287,7 +288,7 @@ api.getUserTasks = {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tasks/user',
|
url: '/tasks/user',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders({
|
||||||
userFieldsToExclude: ['inbox'],
|
userFieldsToInclude: ['_id', 'tasksOrder', 'preferences'],
|
||||||
})],
|
})],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||||
@@ -297,10 +298,10 @@ api.getUserTasks = {
|
|||||||
let validationErrors = req.validationErrors();
|
let validationErrors = req.validationErrors();
|
||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
let user = res.locals.user;
|
const user = res.locals.user;
|
||||||
let dueDate = req.query.dueDate;
|
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);
|
return res.respond(200, tasks);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -490,6 +491,9 @@ api.updateTask = {
|
|||||||
if (sanitizedObj.requiresApproval) {
|
if (sanitizedObj.requiresApproval) {
|
||||||
task.group.approval.required = true;
|
task.group.approval.required = true;
|
||||||
}
|
}
|
||||||
|
if (sanitizedObj.sharedCompletion) {
|
||||||
|
task.group.sharedCompletion = sanitizedObj.sharedCompletion;
|
||||||
|
}
|
||||||
|
|
||||||
setNextDue(task, user);
|
setNextDue(task, user);
|
||||||
let savedTask = await task.save();
|
let savedTask = await task.save();
|
||||||
@@ -653,6 +657,12 @@ api.scoreTask = {
|
|||||||
user.save(),
|
user.save(),
|
||||||
task.save(),
|
task.save(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (task.group && task.group.taskId) {
|
||||||
|
await handleSharedCompletion(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save results and handle request
|
||||||
if (taskOrderPromise) promises.push(taskOrderPromise);
|
if (taskOrderPromise) promises.push(taskOrderPromise);
|
||||||
let results = await Promise.all(promises);
|
let results = await Promise.all(promises);
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ import {
|
|||||||
getTasks,
|
getTasks,
|
||||||
moveTask,
|
moveTask,
|
||||||
} from '../../../libs/taskManager';
|
} from '../../../libs/taskManager';
|
||||||
|
import { handleSharedCompletion } from '../../../libs/groupTasks';
|
||||||
import apiError from '../../../libs/apiError';
|
import apiError from '../../../libs/apiError';
|
||||||
|
|
||||||
let requiredGroupFields = '_id leader tasksOrder name';
|
let requiredGroupFields = '_id leader tasksOrder name';
|
||||||
|
// @TODO: abstract to task lib
|
||||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
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) {
|
function canNotEditTasks (group, user, assignedUserId) {
|
||||||
let isNotGroupLeader = group.leader !== user._id;
|
let isNotGroupLeader = group.leader !== user._id;
|
||||||
@@ -338,7 +341,7 @@ api.approveTask = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove old notifications
|
// Remove old notifications
|
||||||
let managerPromises = [];
|
let approvalPromises = [];
|
||||||
managers.forEach((manager) => {
|
managers.forEach((manager) => {
|
||||||
let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
|
let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
|
||||||
return notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
|
return notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
|
||||||
@@ -346,7 +349,7 @@ api.approveTask = {
|
|||||||
|
|
||||||
if (notificationIndex !== -1) {
|
if (notificationIndex !== -1) {
|
||||||
manager.notifications.splice(notificationIndex, 1);
|
manager.notifications.splice(notificationIndex, 1);
|
||||||
managerPromises.push(manager.save());
|
approvalPromises.push(manager.save());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,9 +365,11 @@ api.approveTask = {
|
|||||||
direction,
|
direction,
|
||||||
});
|
});
|
||||||
|
|
||||||
managerPromises.push(task.save());
|
await handleSharedCompletion(task);
|
||||||
managerPromises.push(assignedUser.save());
|
|
||||||
await Promise.all(managerPromises);
|
approvalPromises.push(task.save());
|
||||||
|
approvalPromises.push(assignedUser.save());
|
||||||
|
await Promise.all(approvalPromises);
|
||||||
|
|
||||||
res.respond(200, task);
|
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 {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
|
import {
|
||||||
|
SHARED_COMPLETION,
|
||||||
|
} from './groupTasks';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import shared from '../../common';
|
import shared from '../../common';
|
||||||
|
|
||||||
@@ -96,6 +99,7 @@ export async function createTasks (req, res, options = {}) {
|
|||||||
if (taskData.requiresApproval) {
|
if (taskData.requiresApproval) {
|
||||||
newTask.group.approval.required = true;
|
newTask.group.approval.required = true;
|
||||||
}
|
}
|
||||||
|
newTask.group.sharedCompletion = taskData.sharedCompletion || SHARED_COMPLETION.default;
|
||||||
} else {
|
} else {
|
||||||
newTask.userId = user._id;
|
newTask.userId = user._id;
|
||||||
}
|
}
|
||||||
@@ -183,11 +187,12 @@ export async function getTasks (req, res, options = {}) {
|
|||||||
limit = 0; // no limit
|
limit = 0; // no limit
|
||||||
}
|
}
|
||||||
|
|
||||||
query = {
|
query.type = 'todo';
|
||||||
userId: user._id,
|
query.completed = true;
|
||||||
type: 'todo',
|
|
||||||
completed: true,
|
if (owner._id === user._id) {
|
||||||
};
|
query.userId = user._id;
|
||||||
|
}
|
||||||
|
|
||||||
sort = {
|
sort = {
|
||||||
dateCompleted: -1,
|
dateCompleted: -1,
|
||||||
|
|||||||
@@ -9,15 +9,19 @@ import url from 'url';
|
|||||||
|
|
||||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
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.
|
// A list of user fields that aren't needed for the route and are not loaded from the db.
|
||||||
// Must be an array
|
// Must be an array
|
||||||
if (userFieldsToExclude) {
|
if (options.userFieldsToExclude) {
|
||||||
return userFieldsToExclude.map(field => {
|
return options.userFieldsToExclude.map(field => {
|
||||||
return `-${field}`; // -${field} means exclude ${field} in mongodb
|
return `-${field}`; // -${field} means exclude ${field} in mongodb
|
||||||
}).join(' ');
|
}).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
|
// Allows GET requests to /user to specify a list of user fields to return instead of the entire doc
|
||||||
// Notifications are always included
|
// Notifications are always included
|
||||||
const urlPath = url.parse(req.url).pathname;
|
const urlPath = url.parse(req.url).pathname;
|
||||||
@@ -50,7 +54,7 @@ export function authWithHeaders (options = {}) {
|
|||||||
apiToken,
|
apiToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = getUserFields(options.userFieldsToExclude, req);
|
const fields = getUserFields(options, req);
|
||||||
const findPromise = fields ? User.findOne(userQuery).select(fields) : User.findOne(userQuery);
|
const findPromise = fields ? User.findOne(userQuery).select(fields) : User.findOne(userQuery);
|
||||||
|
|
||||||
return findPromise
|
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.approval.required'] = taskToSync.group.approval.required;
|
||||||
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
|
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
|
||||||
|
updateCmd.$set['group.sharedCompletion'] = taskToSync.group.sharedCompletion;
|
||||||
|
|
||||||
let taskSchema = Tasks[taskToSync.type];
|
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.approval.required = taskToSync.group.approval.required;
|
||||||
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
|
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
|
||||||
|
matchingTask.group.sharedCompletion = taskToSync.group.sharedCompletion;
|
||||||
|
|
||||||
// sync checklist
|
// sync checklist
|
||||||
if (taskToSync.checklist) {
|
if (taskToSync.checklist) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import baseModel from '../libs/baseModel';
|
|||||||
import { InternalServerError } from '../libs/errors';
|
import { InternalServerError } from '../libs/errors';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { preenHistory } from '../libs/preening';
|
import { preenHistory } from '../libs/preening';
|
||||||
|
import { SHARED_COMPLETION } from '../libs/groupTasks';
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
@@ -111,6 +112,7 @@ export let TaskSchema = new Schema({
|
|||||||
requested: {type: Boolean, default: false},
|
requested: {type: Boolean, default: false},
|
||||||
requestedDate: {type: Date},
|
requestedDate: {type: Date},
|
||||||
},
|
},
|
||||||
|
sharedCompletion: {type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.default},
|
||||||
},
|
},
|
||||||
|
|
||||||
reminders: [{
|
reminders: [{
|
||||||
|
|||||||
Reference in New Issue
Block a user