Merge branch 'develop' into release

This commit is contained in:
Sabe Jones
2019-05-02 14:25:02 -05:00
33 changed files with 514 additions and 255 deletions

View File

@@ -2,6 +2,7 @@
import { import {
validateItemPath, validateItemPath,
getDefaultOwnedGear, getDefaultOwnedGear,
castItemVal,
} from '../../../../../website/server/libs/items/utils'; } from '../../../../../website/server/libs/items/utils';
describe('Items Utils', () => { describe('Items Utils', () => {
@@ -64,4 +65,49 @@ describe('Items Utils', () => {
expect(validateItemPath('items.quests.invalid')).to.equal(false); expect(validateItemPath('items.quests.invalid')).to.equal(false);
}); });
}); });
describe('castItemVal', () => {
it('returns the item val untouched if not an item path', () => {
expect(castItemVal('notitems.gear.owned.item', 'a string')).to.equal('a string');
});
it('returns the item val untouched if an unsupported path', () => {
expect(castItemVal('items.gear.equipped.weapon', 'a string')).to.equal('a string');
expect(castItemVal('items.currentPet', 'a string')).to.equal('a string');
expect(castItemVal('items.special.snowball', 'a string')).to.equal('a string');
});
it('converts values for pets paths to numbers', () => {
expect(castItemVal('items.pets.Wolf-CottonCandyPink', '5')).to.equal(5);
expect(castItemVal('items.pets.Wolf-Invalid', '5')).to.equal(5);
});
it('converts values for eggs paths to numbers', () => {
expect(castItemVal('items.eggs.LionCub', '5')).to.equal(5);
expect(castItemVal('items.eggs.Armadillo', '5')).to.equal(5);
expect(castItemVal('items.eggs.NotAnArmadillo', '5')).to.equal(5);
});
it('converts values for hatching potions paths to numbers', () => {
expect(castItemVal('items.hatchingPotions.Base', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.StarryNight', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.Invalid', '5')).to.equal(5);
});
it('converts values for food paths to numbers', () => {
expect(castItemVal('items.food.Cake_Base', '5')).to.equal(5);
expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5);
});
it('converts values for mounts paths to numbers', () => {
expect(castItemVal('items.mounts.Cactus-Base', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invisible', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invalid', '5')).to.equal(5);
});
it('converts values for quests paths to numbers', () => {
expect(castItemVal('items.quests.atom3', '5')).to.equal(5);
expect(castItemVal('items.quests.invalid', '5')).to.equal(5);
});
});
}); });

View File

@@ -60,4 +60,10 @@ describe('GET /inbox/messages', () => {
expect(messages.length).to.equal(4); expect(messages.length).to.equal(4);
}); });
it('returns only the messages of one conversation', async () => {
const messages = await user.get(`/inbox/messages?conversation=${otherUser.id}`);
expect(messages.length).to.equal(3);
});
}); });

View File

@@ -63,6 +63,38 @@ describe('Groups DELETE /tasks/:id', () => {
}); });
}); });
it('removes deleted taskʾs approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await user.put(`/tasks/${task._id}/`, {
requiresApproval: true,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
await member2.del(`/tasks/${task._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(1);
});
it('unlinks assigned user', async () => { it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`); await user.del(`/tasks/${task._id}`);

View File

@@ -53,18 +53,29 @@ describe('POST /tasks/:id/approve/:userId', () => {
it('approves an assigned user', async () => { it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user'); let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask); let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync(); await member.sync();
expect(member.notifications.length).to.equal(2); expect(member.notifications.length).to.equal(3);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text})); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true; expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id); expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
@@ -77,18 +88,28 @@ describe('POST /tasks/:id/approve/:userId', () => {
}); });
await member2.post(`/tasks/${task._id}/assign/${member._id}`); await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user'); let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask); let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync(); await member.sync();
expect(member.notifications.length).to.equal(2); expect(member.notifications.length).to.equal(3);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text})); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true; expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id); expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
@@ -132,6 +153,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
}); });
await member2.post(`/tasks/${task._id}/assign/${member._id}`); await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`); await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
@@ -141,6 +172,17 @@ describe('POST /tasks/:id/approve/:userId', () => {
}); });
}); });
it('prevents approving a task if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
it('completes master task when single-completion task is approved', async () => { it('completes master task when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo', text: 'shared completion todo',
@@ -151,6 +193,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
@@ -172,6 +224,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let member2Tasks = await member2.get('/tasks/user'); let member2Tasks = await member2.get('/tasks/user');
@@ -193,6 +255,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`); let groupTasks = await user.get(`/tasks/group/${guild._id}`);
@@ -214,6 +286,25 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let member2Tasks = await member2.get('/tasks/user');
let member2SyncedTask = find(member2Tasks, findAssignedTask);
await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);

View File

@@ -51,7 +51,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
}); });
}); });
it('marks as task as needing more work', async () => { it('marks a task as needing more work', async () => {
const initialNotifications = member.notifications.length; const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);
@@ -77,7 +77,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requestedDate).to.equal(undefined); expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct // Check that the notification is correct
expect(member.notifications.length).to.equal(initialNotifications + 1); expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1]; const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -131,7 +131,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requested).to.equal(false); expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined); expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
expect(member.notifications.length).to.equal(initialNotifications + 1); expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1]; const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -167,6 +167,17 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
}); });
await member2.post(`/tasks/${task._id}/assign/${member._id}`); await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`); await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({

View File

@@ -129,6 +129,13 @@ describe('POST /tasks/:id/score/:direction', () => {
let memberTasks = await member.get('/tasks/user'); let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask); let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`); await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`); await member.post(`/tasks/${syncedTask._id}/score/up`);

View File

@@ -113,6 +113,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
expect(syncedTask).to.exist; expect(syncedTask).to.exist;
}); });
it('sends a notification to assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await member.sync();
let groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(member.notifications.length).to.equal(1);
expect(member.notifications[0].type).to.equal('GROUP_TASK_ASSIGNED');
expect(member.notifications[0].taskId).to.equal(groupTask._id);
});
it('assigns a task to multiple users', async () => { it('assigns a task to multiple users', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`); await user.post(`/tasks/${task._id}/assign/${member2._id}`);

View File

@@ -86,6 +86,13 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist; expect(syncedTask).to.not.exist;
}); });
it('removes task assignment notification from unassigned user', async () => {
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(0);
});
it('unassigns a user and only that user from a task', async () => { it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign/${member2._id}`); await user.post(`/tasks/${task._id}/assign/${member2._id}`);

View File

@@ -0,0 +1,44 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('GET /inbox/conversations', () => {
let user;
let otherUser;
let thirdUser;
before(async () => {
[user, otherUser, thirdUser] = await Promise.all([generateUser(), generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await user.post('/members/send-private-message', {
toUserId: thirdUser.id,
message: 'third',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fifth',
});
});
it('returns the conversations', async () => {
const result = await user.get('/inbox/conversations');
expect(result.length).to.be.equal(3);
expect(result[0].user).to.be.equal(user.profile.name);
expect(result[0].username).to.be.equal(user.auth.local.username);
});
});

View File

@@ -1,5 +1,5 @@
<template lang="pug"> <template lang="pug">
b-modal#rebirth(:title="$t('modalAchievement')", size='md', :hide-footer="true", @hidden="reloadPage()") b-modal#rebirth(:title="$t('modalAchievement')", size='md', :hide-footer="true")
.modal-body .modal-body
.col-12 .col-12
// @TODO: +achievementAvatar('sun',0) // @TODO: +achievementAvatar('sun',0)
@@ -41,9 +41,6 @@
close () { close () {
this.$root.$emit('bv::hide::modal', 'rebirth'); this.$root.$emit('bv::hide::modal', 'rebirth');
}, },
reloadPage () {
window.location.reload(true);
},
}, },
}; };
</script> </script>

View File

@@ -376,6 +376,8 @@ export default {
openMemberProgressModal (member) { openMemberProgressModal (member) {
this.$root.$emit('habitica:challenge:member-progress', { this.$root.$emit('habitica:challenge:member-progress', {
progressMemberId: member._id, progressMemberId: member._id,
isLeader: this.isLeader,
isAdmin: this.isAdmin,
}); });
}, },
async exportChallengeCsv () { async exportChallengeCsv () {

View File

@@ -1,6 +1,6 @@
<template lang="pug"> <template lang="pug">
b-modal#challenge-member-modal(title="User Progress", size='lg') b-modal#challenge-member-modal(title="User Progress", size='lg')
.row.award-row .row.award-row(v-if='isLeader || isAdmin')
.col-12.text-center .col-12.text-center
button.btn.btn-primary(v-once, @click='closeChallenge()') {{ $t('awardWinners') }} button.btn.btn-primary(v-once, @click='closeChallenge()') {{ $t('awardWinners') }}
.row .row
@@ -37,12 +37,16 @@ export default {
reward: [], reward: [],
}, },
memberId: '', memberId: '',
isLeader: false,
isAdmin: false,
}; };
}, },
mounted () { mounted () {
this.$root.$on('habitica:challenge:member-progress', (data) => { this.$root.$on('habitica:challenge:member-progress', (data) => {
if (!data.progressMemberId) return; if (!data.progressMemberId) return;
this.memberId = data.progressMemberId; this.memberId = data.progressMemberId;
this.isLeader = data.isLeader;
this.isAdmin = data.isAdmin;
this.$root.$emit('bv::show::modal', 'challenge-member-modal'); this.$root.$emit('bv::show::modal', 'challenge-member-modal');
}); });
}, },

View File

@@ -16,54 +16,7 @@
h1 {{ $t('groupTasksTitle') }} h1 {{ $t('groupTasksTitle') }}
// @TODO: Abstract to component! // @TODO: Abstract to component!
.col-12.col-md-4 .col-12.col-md-4
.input-group
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText") input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
.filter-panel(v-if="isFilterPanelOpen")
.tags-category(v-for="tagsType in tagsByType", v-if="tagsType.tags.length > 0", :key="tagsType.key")
.tags-header.col-12
strong(v-once) {{ $t(tagsType.key) }}
a.d-block(v-if="tagsType.key === 'tags' && !editingTags", @click="editTags()") {{ $t('editTags2') }}
.tags-list.container.col-12
.row(:class="{'no-gutters': !editingTags}")
template(v-if="editingTags && tagsType.key === 'tags'")
.col-12.col-md-6(v-for="(tag, tagIndex) in tagsSnap")
.inline-edit-input-group.tag-edit-item.input-group
input.tag-edit-input.inline-edit-input.form-control(type="text", :value="tag.name")
.input-group-append(@click="removeTag(tagIndex)")
.svg-icon.destroy-icon(v-html="icons.destroy")
.col-12.col-md-6
input.new-tag-item.edit-tag-item.inline-edit-input.form-control(type="text", :placeholder="$t('newTag')", @keydown.enter="addTag($event)", v-model="newTag")
template(v-else)
.col-12.col-md-6(v-for="(tag, tagIndex) in tagsType.tags")
.custom-control.custom-checkbox
input.custom-control-input(
type="checkbox",
:checked="isTagSelected(tag)",
@change="toggleTag(tag)",
:id="`tag-${tagsType.key}-${tagIndex}`",
)
label.custom-control-label(:for="`tag-${tagsType.key}-${tagIndex}`") {{ tag.name }}
.filter-panel-footer.clearfix
template(v-if="editingTags === true")
.text-center
a.mr-3.btn-filters-primary(@click="saveTags()", v-once) {{ $t('saveEdits') }}
a.btn-filters-secondary(@click="cancelTagsEditing()", v-once) {{ $t('cancel') }}
template(v-else)
.float-left
a.btn-filters-danger(@click="resetFilters()", v-once) {{ $t('resetFilters') }}
.float-right
a.mr-3.btn-filters-primary(@click="applyFilters()", v-once) {{ $t('applyFilters') }}
a.btn-filters-secondary(@click="closeFilterPanel()", v-once) {{ $t('cancel') }}
span.input-group-append
button.btn.btn-secondary.filter-button(
type="button",
@click="toggleFilterPanel()",
:class="{'filter-button-open': selectedTags.length > 0}",
)
.d-flex.align-items-center
span(v-once) {{ $t('filter') }}
.svg-icon.filter-icon(v-html="icons.filter")
.create-task-area.d-flex(v-if='canCreateTasks') .create-task-area.d-flex(v-if='canCreateTasks')
transition(name="slide-tasks-btns") transition(name="slide-tasks-btns")
.d-flex(v-if="openCreateBtn") .d-flex(v-if="openCreateBtn")
@@ -99,10 +52,6 @@
@import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/create-task.scss'; @import '~client/assets/scss/create-task.scss';
.user-tasks-page {
padding-top: 31px;
}
.tasks-navigation { .tasks-navigation {
margin-bottom: 40px; margin-bottom: 40px;
} }
@@ -114,133 +63,6 @@
margin-right: 8px; margin-right: 8px;
padding-top: 6px; padding-top: 6px;
} }
.dropdown-icon-item .svg-icon {
width: 16px;
}
button.btn.btn-secondary.filter-button {
box-shadow: none;
border-radius: 2px;
border: 1px solid $gray-400 !important;
&:hover, &:active, &:focus, &.open {
box-shadow: none;
border-color: $purple-500 !important;
color: $gray-50 !important;
}
&.filter-button-open {
color: $purple-200 !important;
.filter-icon {
color: $purple-200 !important;
}
}
.filter-icon {
height: 10px;
width: 12px;
color: $gray-50;
margin-left: 15px;
}
}
.filter-panel {
position: absolute;
padding-left: 24px;
padding-right: 24px;
max-width: 40vw;
width: 100%;
z-index: 9999;
background: $white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
top: 44px;
left: 20vw;
font-size: 14px;
line-height: 1.43;
text-overflow: ellipsis;
.tags-category {
border-bottom: 1px solid $gray-600;
padding-bottom: 24px;
padding-top: 24px;
}
.tags-header {
flex-basis: 96px;
flex-shrink: 0;
a {
font-size: 12px;
line-height: 1.33;
color: $blue-10;
margin-top: 4px;
&:focus, &:hover, &:active {
text-decoration: underline;
}
}
}
.tag-edit-input {
border-bottom: 1px solid $gray-500 !important;
&:focus, &:focus ~ .input-group-append {
border-color: $purple-500 !important;
}
}
.new-tag-item {
width: 100%;
background-repeat: no-repeat;
background-position: center left 10px;
border-bottom: 1px solid $gray-500 !important;
background-size: 10px 10px;
padding-left: 40px;
background-image: url(~client/assets/svg/for-css/positive.svg);
}
.tag-edit-item .input-group-append {
border-bottom: 1px solid $gray-500 !important;
&:focus {
border-color: $purple-500;
}
}
.custom-control-label {
margin-left: 10px;
}
.filter-panel-footer {
padding-top: 16px;
padding-bottom: 16px;
a {
&:focus, &:hover, &:active {
text-decoration: underline;
}
}
.btn-filters-danger {
color: $red-50;
}
.btn-filters-primary {
color: $blue-10;
}
.btn-filters-secondary {
color: $gray-300;
}
}
}
.button-label {
display: inline-block;
}
</style> </style>
<script> <script>

View File

@@ -0,0 +1,26 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
:read-after-click="true",
@click="action",
)
div(slot="content", v-html="notification.data.message")
</template>
<script>
import BaseNotification from './base';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
methods: {
action () {
this.$router.push({ name: 'tasks'});
},
},
};
</script>

View File

@@ -87,6 +87,7 @@ import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import QUEST_INVITATION from './notifications/questInvitation'; import QUEST_INVITATION from './notifications/questInvitation';
import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval'; import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval';
import GROUP_TASK_APPROVED from './notifications/groupTaskApproved'; import GROUP_TASK_APPROVED from './notifications/groupTaskApproved';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints'; import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems'; import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems';
import CARD_RECEIVED from './notifications/cardReceived'; import CARD_RECEIVED from './notifications/cardReceived';
@@ -102,7 +103,7 @@ export default {
// One component for each type // One component for each type
NEW_STUFF, GROUP_TASK_NEEDS_WORK, NEW_STUFF, GROUP_TASK_NEEDS_WORK,
GUILD_INVITATION, PARTY_INVITATION, CHALLENGE_INVITATION, GUILD_INVITATION, PARTY_INVITATION, CHALLENGE_INVITATION,
QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED, QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED, GROUP_TASK_ASSIGNED,
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED, UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE, NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
WorldBoss: WORLD_BOSS, WorldBoss: WORLD_BOSS,
@@ -118,7 +119,7 @@ export default {
openStatus: undefined, openStatus: undefined,
actionableNotifications: [ actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION', 'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK', 'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK', 'GROUP_TASK_APPROVAL',
], ],
// A list of notifications handled by this component, // A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel. // listed in the order they should appear in the notifications panel.
@@ -126,7 +127,7 @@ export default {
handledNotifications: [ handledNotifications: [
'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK', 'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION', 'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED', 'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS', 'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
'VERIFY_USERNAME', 'VERIFY_USERNAME',

View File

@@ -132,11 +132,6 @@ const NOTIFICATIONS = {
label: ($t) => `${$t('achievement')}: ${$t('gearAchievementNotification')}`, label: ($t) => `${$t('achievement')}: ${$t('gearAchievementNotification')}`,
modalId: 'ultimate-gear', modalId: 'ultimate-gear',
}, },
REBIRTH_ACHIEVEMENT: {
label: ($t) => `${$t('achievement')}: ${$t('rebirthBegan')}`,
achievement: true,
modalId: 'rebirth',
},
GUILD_JOINED_ACHIEVEMENT: { GUILD_JOINED_ACHIEVEMENT: {
label: ($t) => `${$t('achievement')}: ${$t('joinedGuild')}`, label: ($t) => `${$t('achievement')}: ${$t('joinedGuild')}`,
achievement: true, achievement: true,
@@ -360,18 +355,7 @@ export default {
this.playSound(config.sound); this.playSound(config.sound);
} }
if (type === 'REBIRTH_ACHIEVEMENT') { if (forceToModal) {
// reload if the user hasn't clicked on the notification
const timeOut = setTimeout(() => {
window.location.reload(true);
}, 60000);
this.text(config.label(this.$t), () => {
// prevent the current reload timeout
clearTimeout(timeOut);
this.$root.$emit('bv::show::modal', config.modalId);
}, false);
} else if (forceToModal) {
this.$root.$emit('bv::show::modal', config.modalId); this.$root.$emit('bv::show::modal', config.modalId);
} else { } else {
this.text(config.label(this.$t), () => { this.text(config.label(this.$t), () => {
@@ -573,8 +557,11 @@ export default {
}, this.user.preferences.suppressModals.streak); }, this.user.preferences.suppressModals.streak);
this.playSound('Achievement_Unlocked'); this.playSound('Achievement_Unlocked');
break; break;
case 'ULTIMATE_GEAR_ACHIEVEMENT':
case 'REBIRTH_ACHIEVEMENT': case 'REBIRTH_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
break;
case 'ULTIMATE_GEAR_ACHIEVEMENT':
case 'GUILD_JOINED_ACHIEVEMENT': case 'GUILD_JOINED_ACHIEVEMENT':
case 'CHALLENGE_JOINED_ACHIEVEMENT': case 'CHALLENGE_JOINED_ACHIEVEMENT':
case 'INVITED_FRIEND_ACHIEVEMENT': case 'INVITED_FRIEND_ACHIEVEMENT':

View File

@@ -75,7 +75,7 @@
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}" :class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
) {{ $t('buyNow') }} ) {{ $t('buyNow') }}
div.limitedTime(v-if="item.event") div.limitedTime(v-if="item.event && item.owned == null")
span.svg-icon.inline.icon-16(v-html="icons.clock") span.svg-icon.inline.icon-16(v-html="icons.clock")
span.limitedString {{ limitedString }} span.limitedString {{ limitedString }}
@@ -397,6 +397,10 @@
this.$emit('buyPressed', this.item); this.$emit('buyPressed', this.item);
this.hideDialog(); this.hideDialog();
if (this.item.key === 'rebirth_orb') {
window.location.reload(true);
}
}, },
purchaseGems () { purchaseGems () {
if (this.item.key === 'rebirth_orb') { if (this.item.key === 'rebirth_orb') {

View File

@@ -5,7 +5,7 @@ div
slot(name="itemBadge", :item="item", :emptyItem="emptyItem") slot(name="itemBadge", :item="item", :emptyItem="emptyItem")
span.badge.badge-pill.badge-item.badge-clock( span.badge.badge-pill.badge-item.badge-clock(
v-if="item.event && showEventBadge", v-if="item.event && item.owned == null && showEventBadge",
) )
span.svg-icon.inline.clock(v-html="icons.clock") span.svg-icon.inline.clock(v-html="icons.clock")

View File

@@ -1,11 +1,15 @@
<template lang="pug"> <template lang="pug">
.tags-popup .tags-popup
.tags-category.d-flex .tags-category.d-flex(
v-for="tagsType in tagsByType",
v-if="tagsType.tags.length > 0 || tagsType.key === 'tags'",
:key="tagsType.key"
)
.tags-header .tags-header
strong(v-once) {{ $t('tags') }} strong(v-once) {{ $t(tagsType.key) }}
.tags-list.container .tags-list.container
.row .row
.col-4(v-for="tag in tags") .col-4(v-for="(tag, tagIndex) in tagsType.tags")
.custom-control.custom-checkbox .custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value="tag.id", v-model="selectedTags", :id="`tag-${tag.id}`") input.custom-control-input(type="checkbox", :value="tag.id", v-model="selectedTags", :id="`tag-${tag.id}`")
label.custom-control-label(:title="tag.name", :for="`tag-${tag.id}`", v-markdown="tag.name") label.custom-control-label(:title="tag.name", :for="`tag-${tag.id}`", v-markdown="tag.name")
@@ -103,6 +107,34 @@ export default {
selectedTags: [], selectedTags: [],
}; };
}, },
computed: {
tagsByType () {
const tagsByType = {
challenges: {
key: 'challenges',
tags: [],
},
groups: {
key: 'groups',
tags: [],
},
user: {
key: 'tags',
tags: [],
},
};
this.$props.tags.forEach(t => {
if (t.group) {
tagsByType.groups.tags.push(t);
} else if (t.challenge) {
tagsByType.challenges.tags.push(t);
} else {
tagsByType.user.tags.push(t);
}
});
return tagsByType;
},
},
watch: { watch: {
selectedTags () { selectedTags () {
this.$emit('input', this.selectedTags); this.$emit('input', this.selectedTags);

View File

@@ -159,6 +159,15 @@
| {{ $t(frequency) }} | {{ $t(frequency) }}
.option.group-options(v-if='groupId') .option.group-options(v-if='groupId')
.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) }}
.form-group.row .form-group.row
label.col-12(v-once) {{ $t('assignedTo') }} label.col-12(v-once) {{ $t('assignedTo') }}
.col-12.mt-2 .col-12.mt-2
@@ -179,23 +188,12 @@
.row .row
button.btn.btn-primary(@click.stop.prevent="showAssignedSelect = !showAssignedSelect") {{$t('close')}} button.btn.btn-primary(@click.stop.prevent="showAssignedSelect = !showAssignedSelect") {{$t('close')}}
.option.group-options(v-if='groupId')
.form-group .form-group
label(v-once) {{ $t('approvalRequired') }} label(v-once) {{ $t('approvalRequired') }}
toggle-switch.d-inline-block( toggle-switch.d-inline-block(
: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")
@@ -360,8 +358,8 @@
margin-top: 12px; margin-top: 12px;
position: relative; position: relative;
label { .custom-control-label p {
max-height: 30px; word-break: break-word;
} }
} }
@@ -711,7 +709,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', sharedCompletion: 'singleCompletion',
members: [], members: [],
memberNamesById: {}, memberNamesById: {},
assignedMembers: [], assignedMembers: [],
@@ -842,7 +840,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'; if (this.task.group) this.sharedCompletion = this.task.group.sharedCompletion || 'singleCompletion';
} }
// @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
@@ -926,7 +924,6 @@ export default {
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks // 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.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.sharedCompletion = this.sharedCompletion;
@@ -954,6 +951,7 @@ export default {
}); });
}); });
Promise.all(promises); Promise.all(promises);
this.task.group.assignedUsers = this.assignedMembers;
this.$emit('taskCreated', this.task); this.$emit('taskCreated', this.task);
} else { } else {
this.createTask(this.task); this.createTask(this.task);

View File

@@ -201,6 +201,7 @@
.custom-control-label { .custom-control-label {
margin-left: 10px; margin-left: 10px;
word-break: break-word;
} }
.filter-panel-footer { .filter-panel-footer {

View File

@@ -51,10 +51,8 @@ export async function set (store, changes) {
} }
} }
axios.put('/api/v4/user', changes); let response = await axios.put('/api/v4/user', changes);
// TODO return response.data.data;
// .then((res) => console.log('set', res))
// .catch((err) => console.error('set', err));
} }
export async function sleep (store) { export async function sleep (store) {

View File

@@ -285,7 +285,8 @@
"claim": "Claim", "claim": "Claim",
"removeClaim": "Remove Claim", "removeClaim": "Remove Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription", "onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green\"><%= taskText %></span> has been approved.", "youHaveBeenAssignedTask": "<%= managerName %> has assigned you the task <span class=\"notification-bold\"><%= taskText %></span>.",
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green notification-bold\"><%= taskText %></span> has been approved.",
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.", "taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.",
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>", "userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>",
"approve": "Approve", "approve": "Approve",

View File

@@ -173,7 +173,7 @@
"habitCounterDown": "Negative Counter (Resets <%= frequency %>)", "habitCounterDown": "Negative Counter (Resets <%= frequency %>)",
"taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested", "taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested",
"taskApprovalHasBeenRequested": "Approval has been requested", "taskApprovalHasBeenRequested": "Approval has been requested",
"taskApprovalWasNotRequested": "Only a task waiting for approval can be marked as needing more work", "taskApprovalWasNotRequested": "Approval has not been requested for this task.",
"approvals": "Approvals", "approvals": "Approvals",
"approvalRequired": "Needs Approval", "approvalRequired": "Needs Approval",
"repeatZero": "Daily is never due", "repeatZero": "Daily is never due",

View File

@@ -7,7 +7,10 @@ import {
import _ from 'lodash'; import _ from 'lodash';
import apiError from '../../libs/apiError'; import apiError from '../../libs/apiError';
import validator from 'validator'; import validator from 'validator';
import { validateItemPath } from '../../libs/items/utils'; import {
validateItemPath,
castItemVal,
} from '../../libs/items/utils';
let api = {}; let api = {};
@@ -271,7 +274,7 @@ api.updateHero = {
hero.markModified('items.pets'); hero.markModified('items.pets');
} }
if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) { if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) {
_.set(hero, updateData.itemPath, updateData.itemVal); // Sanitization at 5c30944 (deemed unnecessary) _.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal)); // Sanitization at 5c30944 (deemed unnecessary)
} }
if (updateData.auth && updateData.auth.blocked === true) { if (updateData.auth && updateData.auth.blocked === true) {

View File

@@ -12,6 +12,7 @@ let api = {};
* @apiDescription Get inbox messages for a user * @apiDescription Get inbox messages for a user
* *
* @apiParam (Query) {Number} page Load the messages of the selected Page - 10 Messages per Page * @apiParam (Query) {Number} page Load the messages of the selected Page - 10 Messages per Page
* @apiParam (Query) {GUID} conversation Loads only the messages of a conversation
* *
* @apiSuccess {Array} data An array of inbox messages * @apiSuccess {Array} data An array of inbox messages
*/ */
@@ -22,9 +23,10 @@ api.getInboxMessages = {
async handler (req, res) { async handler (req, res) {
const user = res.locals.user; const user = res.locals.user;
const page = req.query.page; const page = req.query.page;
const conversation = req.query.conversation;
const userInbox = await inboxLib.getUserInbox(user, { const userInbox = await inboxLib.getUserInbox(user, {
page, page, conversation,
}); });
res.respond(200, userInbox); res.respond(200, userInbox);

View File

@@ -206,6 +206,14 @@ api.assignTask = {
let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text}); let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text});
const newMessage = group.sendChat(message); const newMessage = group.sendChat(message);
promises.push(newMessage.save()); promises.push(newMessage.save());
} else {
const taskText = task.text;
const managerName = user.profile.name;
assignedUser.addNotification('GROUP_TASK_ASSIGNED', {
message: res.t('youHaveBeenAssignedTask', {managerName, taskText}),
taskId: task._id,
});
} }
promises.push(group.syncTask(task, assignedUser)); promises.push(group.syncTask(task, assignedUser));
@@ -261,6 +269,15 @@ api.unassignTask = {
await group.unlinkTask(task, assignedUser); await group.unlinkTask(task, assignedUser);
let notificationIndex = assignedUser.notifications.findIndex(function findNotification (notification) {
return notification && notification.data && notification.type === 'GROUP_TASK_ASSIGNED' && notification.data.taskId === task._id;
});
if (notificationIndex !== -1) {
assignedUser.notifications.splice(notificationIndex, 1);
await assignedUser.save();
}
res.respond(200, task); res.respond(200, task);
}, },
}; };
@@ -308,6 +325,9 @@ api.approveTask = {
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (task.group.approval.approved === true) throw new NotAuthorized(res.t('canOnlyApproveTaskOnce')); if (task.group.approval.approved === true) throw new NotAuthorized(res.t('canOnlyApproveTaskOnce'));
if (!task.group.approval.requested) {
throw new NotAuthorized(res.t('taskApprovalWasNotRequested'));
}
task.group.approval.dateApproved = new Date(); task.group.approval.dateApproved = new Date();
task.group.approval.approvingUser = user._id; task.group.approval.approvingUser = user._id;

View File

@@ -72,4 +72,25 @@ api.clearMessages = {
}, },
}; };
/**
* @api {get} /inbox/conversations Get the conversations for a user
* @apiName conversations
* @apiGroup Inbox
* @apiDescription Get the conversations for a user
*
* @apiSuccess {Array} data An array of inbox conversations
*/
api.conversations = {
method: 'GET',
middlewares: [authWithHeaders()],
url: '/inbox/conversations',
async handler (req, res) {
const user = res.locals.user;
const result = await inboxLib.listConversations(user);
res.respond(200, result);
},
};
module.exports = api; module.exports = api;

View File

@@ -1,14 +1,25 @@
import {inboxModel as Inbox} from '../../models/message'; import {inboxModel as Inbox} from '../../models/message';
import {
model as User,
} from '../../models/user';
import orderBy from 'lodash/orderBy';
import keyBy from 'lodash/keyBy';
const PM_PER_PAGE = 10; const PM_PER_PAGE = 10;
export async function getUserInbox (user, options = {asArray: true, page: 0}) { export async function getUserInbox (user, options = {asArray: true, page: 0, conversation: null}) {
if (typeof options.asArray === 'undefined') { if (typeof options.asArray === 'undefined') {
options.asArray = true; options.asArray = true;
} }
const findObj = {ownerId: user._id};
if (options.conversation) {
findObj.uuid = options.conversation;
}
let query = Inbox let query = Inbox
.find({ownerId: user._id}) .find(findObj)
.sort({timestamp: -1}); .sort({timestamp: -1});
if (typeof options.page !== 'undefined') { if (typeof options.page !== 'undefined') {
@@ -29,6 +40,39 @@ export async function getUserInbox (user, options = {asArray: true, page: 0}) {
} }
} }
export async function listConversations (user) {
let query = Inbox
.aggregate([
{
$match: {
ownerId: user._id,
},
},
{
$group: {
_id: '$uuid',
timestamp: {$max: '$timestamp'}, // sort before group doesn't work - use the max value to sort it again after
},
},
]);
const conversationsList = orderBy(await query.exec(), ['timestamp'], ['desc']).map(c => c._id);
const users = await User.find({_id: {$in: conversationsList}})
.select('_id profile.name auth.local.username')
.lean()
.exec();
const usersMap = keyBy(users, '_id');
const conversations = conversationsList.map(userId => ({
uuid: usersMap[userId]._id,
user: usersMap[userId].profile.name,
username: usersMap[userId].auth.local.username,
}));
return conversations;
}
export async function getUserInboxMessage (user, messageId) { export async function getUserInboxMessage (user, messageId) {
return Inbox.findOne({ownerId: user._id, _id: messageId}).exec(); return Inbox.findOne({ownerId: user._id, _id: messageId}).exec();
} }

View File

@@ -55,3 +55,23 @@ export function validateItemPath (itemPath) {
return Boolean(shared.content.quests[key]); return Boolean(shared.content.quests[key]);
} }
} }
// When passed a value of an item in the user object it'll convert the
// value to the correct format.
// Example a numeric string like "5" applied to a food item (expecting an interger)
// will be converted to the number 5
// TODO cast the correct value for `items.gear.owned`
export function castItemVal (itemPath, itemVal) {
if (
itemPath.indexOf('items.pets') === 0 ||
itemPath.indexOf('items.eggs') === 0 ||
itemPath.indexOf('items.hatchingPotions') === 0 ||
itemPath.indexOf('items.food') === 0 ||
itemPath.indexOf('items.mounts') === 0 ||
itemPath.indexOf('items.quests') === 0
) {
return Number(itemVal);
}
return itemVal;
}

View File

@@ -1341,7 +1341,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
matchingTask.group.id = taskToSync.group.id; matchingTask.group.id = taskToSync.group.id;
matchingTask.userId = user._id; matchingTask.userId = user._id;
matchingTask.group.taskId = taskToSync._id; matchingTask.group.taskId = taskToSync._id;
user.tasksOrder[`${taskToSync.type}s`].push(matchingTask._id); user.tasksOrder[`${taskToSync.type}s`].unshift(matchingTask._id);
} else { } else {
_.merge(matchingTask, syncableAttrs(taskToSync)); _.merge(matchingTask, syncableAttrs(taskToSync));
// Make sure the task is in user.tasksOrder // Make sure the task is in user.tasksOrder
@@ -1419,9 +1419,29 @@ schema.methods.removeTask = async function groupRemoveTask (task) {
$set: {'group.broken': 'TASK_DELETED'}, $set: {'group.broken': 'TASK_DELETED'},
}, {multi: true}).exec(); }, {multi: true}).exec();
// Get Managers
const managerIds = Object.keys(group.managers);
managerIds.push(group.leader);
const managers = await User.find({_id: managerIds}, 'notifications').exec(); // Use this method so we can get access to notifications
// Remove old notifications
let removalPromises = [];
managers.forEach((manager) => {
let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
return notification && notification.data && notification.data.groupTaskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
});
if (notificationIndex !== -1) {
manager.notifications.splice(notificationIndex, 1);
removalPromises.push(manager.save());
}
});
removeFromArray(group.tasksOrder[`${task.type}s`], task._id); removeFromArray(group.tasksOrder[`${task.type}s`], task._id);
group.markModified('tasksOrder'); group.markModified('tasksOrder');
return await group.save(); removalPromises.push(group.save());
return await Promise.all(removalPromises);
}; };
// Returns true if the user has reached the spam message limit // Returns true if the user has reached the spam message limit

View File

@@ -113,7 +113,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}, sharedCompletion: {$type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.single},
}, },
reminders: [{ reminders: [{

View File

@@ -15,6 +15,7 @@ const NOTIFICATION_TYPES = [
'CRON', 'CRON',
'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED', 'GROUP_TASK_APPROVED',
'GROUP_TASK_ASSIGNED',
'GROUP_TASK_NEEDS_WORK', 'GROUP_TASK_NEEDS_WORK',
'LOGIN_INCENTIVE', 'LOGIN_INCENTIVE',
'GROUP_INVITE_ACCEPTED', 'GROUP_INVITE_ACCEPTED',