diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js index 9972d6c33f..c4a65c72fa 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js @@ -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); + }); }); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js index d50841d3bc..41f6c2ae2b 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js @@ -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); + }); }); diff --git a/website/client/app.vue b/website/client/app.vue index 1133f0823b..400991d4d7 100644 --- a/website/client/app.vue +++ b/website/client/app.vue @@ -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}); }); }, diff --git a/website/client/assets/scss/markdown.scss b/website/client/assets/scss/markdown.scss index 3aba216118..9e4c2a8926 100644 --- a/website/client/assets/scss/markdown.scss +++ b/website/client/assets/scss/markdown.scss @@ -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; } diff --git a/website/client/components/challenges/challengeModal.vue b/website/client/components/challenges/challengeModal.vue index 0b4637505b..84e328a8c6 100644 --- a/website/client/components/challenges/challengeModal.vue +++ b/website/client/components/challenges/challengeModal.vue @@ -317,14 +317,17 @@ export default { methods: { async shown () { this.groups = await this.$store.dispatch('guilds:getMyGuilds'); - await this.$store.dispatch('party:getParty'); - const party = this.$store.state.party.data; - if (party._id) { - this.groups.push({ - name: party.name, - _id: party._id, - privacy: 'private', - }); + + if (this.user.party && this.user.party._id) { + await this.$store.dispatch('party:getParty'); + const party = this.$store.state.party.data; + if (party._id) { + this.groups.push({ + name: party.name, + _id: party._id, + privacy: 'private', + }); + } } this.groups.push({ diff --git a/website/client/components/chat/chatCard.vue b/website/client/components/chat/chatCard.vue index a8fb593609..3776ac2237 100644 --- a/website/client/components/chat/chatCard.vue +++ b/website/client/components/chat/chatCard.vue @@ -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(); }, }, diff --git a/website/client/components/group-plans/taskInformation.vue b/website/client/components/group-plans/taskInformation.vue index 610ee98929..01aeb6c3e8 100644 --- a/website/client/components/group-plans/taskInformation.vue +++ b/website/client/components/group-plans/taskInformation.vue @@ -80,6 +80,7 @@ :key="column", :taskListOverride='tasksByType[column]', v-on:editTask="editTask", + v-on:loadGroupCompletedTodos="loadGroupCompletedTodos", :group='group', :searchText="searchText") @@ -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: ''}); diff --git a/website/client/components/groups/createPartyModal.vue b/website/client/components/groups/createPartyModal.vue index 081e8d3ed2..42d168bafc 100644 --- a/website/client/components/groups/createPartyModal.vue +++ b/website/client/components/groups/createPartyModal.vue @@ -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 @@ -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'); + }, }, }; diff --git a/website/client/components/groups/publicGuildItem.vue b/website/client/components/groups/publicGuildItem.vue index a08548eb17..c7b7883314 100644 --- a/website/client/components/groups/publicGuildItem.vue +++ b/website/client/components/groups/publicGuildItem.vue @@ -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 { diff --git a/website/client/components/header/userDropdown.vue b/website/client/components/header/userDropdown.vue index 788197c37f..bcbfcfd077 100644 --- a/website/client/components/header/userDropdown.vue +++ b/website/client/components/header/userDropdown.vue @@ -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') }} diff --git a/website/client/components/notifications.vue b/website/client/components/notifications.vue index d255437879..6ae21ff7e4 100644 --- a/website/client/components/notifications.vue +++ b/website/client/components/notifications.vue @@ -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.$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; case 'INVITED_FRIEND_ACHIEVEMENT': this.playSound('Achievement_Unlocked'); diff --git a/website/client/components/payments/amazonModal.vue b/website/client/components/payments/amazonModal.vue index 4d31910236..a791978891 100644 --- a/website/client/components/payments/amazonModal.vue +++ b/website/client/components/payments/amazonModal.vue @@ -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", diff --git a/website/client/components/payments/sendGemsModal.vue b/website/client/components/payments/sendGemsModal.vue index b51bf87b2b..98d32993c9 100644 --- a/website/client/components/payments/sendGemsModal.vue +++ b/website/client/components/payments/sendGemsModal.vue @@ -1,5 +1,5 @@