Added notification for when leader is updated (#9674)

* Added notification for when leader is updated

* Abstracted challenge member search component

* Added challenge member search modal to challenge detail

* Added group search
This commit is contained in:
Keith Holliday
2017-12-14 12:12:43 -06:00
committed by GitHub
parent 54db84fddc
commit c28ec24c33
11 changed files with 162 additions and 33 deletions

View File

@@ -161,4 +161,19 @@ describe('GET /groups/:groupId/members', () => {
let resIds = res.concat(res2).map(member => member._id); let resIds = res.concat(res2).map(member => member._id);
expect(resIds).to.eql(expectedIds.sort()); expect(resIds).to.eql(expectedIds.sort());
}); });
it('searches members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let usersToGenerate = [];
for (let i = 0; i < 2; i++) {
usersToGenerate.push(generateUser({party: {_id: group._id}}));
}
const usersCreated = await Promise.all(usersToGenerate);
const userToSearch = usersCreated[0].profile.name;
let res = await user.get(`/groups/party/members?search=${userToSearch}`);
expect(res.length).to.equal(1);
expect(res[0].profile.name).to.equal(userToSearch);
});
}); });

View File

@@ -33,10 +33,7 @@
.col-7.offset-5 .col-7.offset-5
span.view-progress span.view-progress
strong {{ $t('viewProgressOf') }} strong {{ $t('viewProgressOf') }}
b-dropdown.create-dropdown(text="Select a Participant") member-search-dropdown(:text="$t('selectParticipant')", :members='members', :challengeId='challengeId', @member-selected='openMemberProgressModal')
input.form-control(type='text', v-model='searchTerm')
b-dropdown-item(v-for="member in memberResults", :key="member._id", @click="openMemberProgressModal(member._id)")
| {{ member.profile.name }}
span(v-if='isLeader || isAdmin') span(v-if='isLeader || isAdmin')
b-dropdown.create-dropdown(:text="$t('addTaskToChallenge')", :variant="'success'") b-dropdown.create-dropdown(:text="$t('addTaskToChallenge')", :variant="'success'")
b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)") b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)")
@@ -51,7 +48,6 @@
v-on:taskEdited='taskEdited', v-on:taskEdited='taskEdited',
@taskDestroyed='taskDestroyed' @taskDestroyed='taskDestroyed'
) )
.row .row
task-column.col-12.col-sm-6( task-column.col-12.col-sm-6(
v-for="column in columns", v-for="column in columns",
@@ -185,6 +181,7 @@ import omit from 'lodash/omit';
import uuid from 'uuid'; import uuid from 'uuid';
import { mapState } from 'client/libs/store'; import { mapState } from 'client/libs/store';
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
import closeChallengeModal from './closeChallengeModal'; import closeChallengeModal from './closeChallengeModal';
import Column from '../tasks/column'; import Column from '../tasks/column';
import TaskModal from '../tasks/taskModal'; import TaskModal from '../tasks/taskModal';
@@ -211,6 +208,7 @@ export default {
leaveChallengeModal, leaveChallengeModal,
challengeModal, challengeModal,
challengeMemberProgressModal, challengeMemberProgressModal,
memberSearchDropdown,
TaskColumn: Column, TaskColumn: Column,
TaskModal, TaskModal,
}, },
@@ -388,8 +386,8 @@ export default {
updatedChallenge (eventData) { updatedChallenge (eventData) {
Object.assign(this.challenge, eventData.challenge); Object.assign(this.challenge, eventData.challenge);
}, },
openMemberProgressModal (memberId) { openMemberProgressModal (member) {
this.progressMemberId = memberId; this.progressMemberId = member._id;
this.$root.$emit('bv::show::modal', 'challenge-member-modal'); this.$root.$emit('bv::show::modal', 'challenge-member-modal');
}, },
async exportChallengeCsv () { async exportChallengeCsv () {

View File

@@ -10,10 +10,7 @@ div
.col-12 .col-12
strong(v-once) {{$t('selectChallengeWinnersDescription')}} strong(v-once) {{$t('selectChallengeWinnersDescription')}}
.col-12 .col-12
b-dropdown.create-dropdown(:text="winnerText") member-search-dropdown(:text='winnerText', :members='members', :challengeId='challengeId', @member-selected='selectMember')
input.form-control(type='text', v-model='searchTerm')
b-dropdown-item(v-for="member in memberResults", :key="member._id", @click="selectMember(member)")
| {{ member.profile.name }}
.col-12 .col-12
button.btn.btn-primary(v-once, @click='closeChallenge') {{$t('awardWinners')}} button.btn.btn-primary(v-once, @click='closeChallenge') {{$t('awardWinners')}}
.col-12 .col-12
@@ -74,16 +71,16 @@ div
</style> </style>
<script> <script>
import challengeMemberSearchMixin from 'client/mixins/challengeMemberSearch'; import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
export default { export default {
props: ['challengeId', 'members'], props: ['challengeId', 'members'],
mixins: [challengeMemberSearchMixin], components: {
memberSearchDropdown,
},
data () { data () {
return { return {
winner: {}, winner: {},
searchTerm: '',
memberResults: [],
}; };
}, },
computed: { computed: {

View File

@@ -8,9 +8,7 @@
.form-group(v-if='workingGroup.id && members.length > 0') .form-group(v-if='workingGroup.id && members.length > 0')
label label
strong(v-once) {{$t('guildOrPartyLeader')}} * strong(v-once) {{$t('guildOrPartyLeader')}} *
select.form-control(v-model="workingGroup.newLeader") group-member-search-dropdown(:text="currentLeader", :members='members', :groupId='workingGroup.id', @member-selected='selectNewLeader')
option(v-for='potentialLeader in potentialLeaders', :value="potentialLeader._id") {{ potentialLeader.name }}
.form-group .form-group
label label
strong(v-once) {{$t('privacySettings')}} * strong(v-once) {{$t('privacySettings')}} *
@@ -170,6 +168,7 @@
<script> <script>
import { mapState } from 'client/libs/store'; import { mapState } from 'client/libs/store';
import toggleSwitch from 'client/components/ui/toggleSwitch'; import toggleSwitch from 'client/components/ui/toggleSwitch';
import groupMemberSearchDropdown from 'client/components/members/groupMemberSearchDropdown';
import markdownDirective from 'client/directives/markdown'; import markdownDirective from 'client/directives/markdown';
import gemIcon from 'assets/svg/gem.svg'; import gemIcon from 'assets/svg/gem.svg';
import informationIcon from 'assets/svg/information.svg'; import informationIcon from 'assets/svg/information.svg';
@@ -185,6 +184,7 @@ import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../common/script/constants';
export default { export default {
components: { components: {
toggleSwitch, toggleSwitch,
groupMemberSearchDropdown,
}, },
directives: { directives: {
markdown: markdownDirective, markdown: markdownDirective,
@@ -307,16 +307,12 @@ export default {
isParty () { isParty () {
return this.workingGroup.type === 'party'; return this.workingGroup.type === 'party';
}, },
potentialLeaders () { currentLeader () {
let leaders = [{ _id: this.user._id, name: this.user.profile.name }]; const currentLeader = this.members.find(member => {
// @TODO consider pushing all recent posters to the top of the list if they are guild members - more likely to be the ones the leader wants to see (and then ignore them in the while below) return member._id === this.workingGroup.newLeader;
let i = 0; });
while (this.members[i]) { const currentLeaderName = currentLeader.profile ? currentLeader.profile.name : '';
let memb = this.members[i]; return currentLeaderName;
i++;
if (memb._id !== this.user._id) leaders.push({_id: memb._id, name: memb.profile.name});
}
return leaders;
}, },
}, },
watch: { watch: {
@@ -356,6 +352,9 @@ export default {
}, },
}, },
methods: { methods: {
selectNewLeader (member) {
this.workingGroup.newLeader = member._id;
},
async getMembers () { async getMembers () {
if (!this.workingGroup.id) return; if (!this.workingGroup.id) return;
let members = await this.$store.dispatch('members:getGroupMembers', { let members = await this.$store.dispatch('members:getGroupMembers', {

View File

@@ -36,7 +36,7 @@ div
span.dropdown-icon-item span.dropdown-icon-item
.svg-icon.inline(v-html="icons.messageIcon") .svg-icon.inline(v-html="icons.messageIcon")
span.text {{$t('sendMessage')}} span.text {{$t('sendMessage')}}
b-dropdown-item(@click='promoteToLeader(member._id)', v-if='isLeader') b-dropdown-item(@click='promoteToLeader(member)', v-if='isLeader || isAdmin')
span.dropdown-icon-item span.dropdown-icon-item
.svg-icon.inline(v-html="icons.starIcon") .svg-icon.inline(v-html="icons.starIcon")
span.text {{$t('promoteToLeader')}} span.text {{$t('promoteToLeader')}}
@@ -290,6 +290,9 @@ export default {
if (!this.group || !this.group.leader) return false; if (!this.group || !this.group.leader) return false;
return this.user._id === this.group.leader || this.user._id === this.group.leader._id; return this.user._id === this.group.leader || this.user._id === this.group.leader._id;
}, },
isAdmin () {
return Boolean(this.user.contributor.admin);
},
groupIsSubscribed () { groupIsSubscribed () {
return this.group.purchased.active; return this.group.purchased.active;
}, },
@@ -440,10 +443,15 @@ export default {
}); });
this.viewMembers(); this.viewMembers();
}, },
async promoteToLeader (memberId) { async promoteToLeader (member) {
let groupData = Object.assign({}, this.group); let groupData = Object.assign({}, this.group);
groupData.leader = memberId;
groupData.leader = member._id;
await this.$store.dispatch('guilds:update', {group: groupData}); await this.$store.dispatch('guilds:update', {group: groupData});
alert(this.$t('leaderChanged'));
groupData.leader = member;
this.$root.$emit('updatedGroup', groupData); this.$root.$emit('updatedGroup', groupData);
}, },
}, },

View File

@@ -0,0 +1,58 @@
<template lang="pug">
b-dropdown.select-member(:text="text")
input.form-control(type='text', v-model='searchTerm')
b-dropdown-item(v-for="member in memberResults", :key="member._id", @click="selectMember(member)")
| {{ member.profile.name }}
</template>
<style lang="scss" scoped>
.select-member {
width: 100%;
}
</style>
<script>
// @TODO: how do we subclass the other member search or compose?
import debounce from 'lodash/debounce';
export default {
props: {
text: {
type: String,
required: true,
},
members: {
type: Array,
required: true,
},
groupId: {
type: String,
},
},
data () {
return {
searchTerm: '',
memberResults: [],
};
},
mounted () {
this.memberResults = this.members;
},
watch: {
searchTerm: debounce(function searchTerm (newSearch) {
this.searchMember(newSearch);
}, 500),
},
methods: {
selectMember (member) {
this.$emit('member-selected', member);
},
async searchMember (search) {
this.memberResults = await this.$store.dispatch('members:getGroupMembers', {
groupId: this.groupId,
searchTerm: search,
});
},
},
};
</script>

View File

@@ -0,0 +1,42 @@
<template lang="pug">
b-dropdown.create-dropdown(:text="text")
input.form-control(type='text', v-model='searchTerm')
b-dropdown-item(v-for="member in memberResults", :key="member._id", @click="selectMember(member)")
| {{ member.profile.name }}
</template>
<style lang="scss" scoped>
</style>
<script>
// @TODO: how do we subclass this rather than type checking?
import challengeMemberSearchMixin from 'client/mixins/challengeMemberSearch';
export default {
mixins: [challengeMemberSearchMixin],
props: {
text: {
type: String,
required: true,
},
members: {
type: Array,
required: true,
},
challengeId: {
type: String,
},
},
data () {
return {
searchTerm: '',
memberResults: [],
};
},
methods: {
selectMember (member) {
this.$emit('member-selected', member);
},
},
};
</script>

View File

@@ -6,9 +6,15 @@ let apiV3Prefix = '/api/v3';
export async function getGroupMembers (store, payload) { export async function getGroupMembers (store, payload) {
let url = `${apiV3Prefix}/groups/${payload.groupId}/members`; let url = `${apiV3Prefix}/groups/${payload.groupId}/members`;
if (payload.includeAllPublicFields) { if (payload.includeAllPublicFields) {
url += '?includeAllPublicFields=true'; url += '?includeAllPublicFields=true';
} }
if (payload.searchTerm) {
url += `?search=${payload.searchTerm}`;
}
let response = await axios.get(url); let response = await axios.get(url);
return response.data.data; return response.data.data;
} }

View File

@@ -130,5 +130,6 @@
"categoiresRequired": "One or more categories must be selected", "categoiresRequired": "One or more categories must be selected",
"viewProgressOf": "View Progress Of", "viewProgressOf": "View Progress Of",
"selectMember": "Select Member", "selectMember": "Select Member",
"confirmKeepChallengeTasks": "Do you want to keep challenge tasks?" "confirmKeepChallengeTasks": "Do you want to keep challenge tasks?",
"selectParticipant": "Select a Participant"
} }

View File

@@ -413,5 +413,6 @@
"groupBilling": "Group Billing", "groupBilling": "Group Billing",
"wouldYouParticipate": "Would you like to participate?", "wouldYouParticipate": "Would you like to participate?",
"managerAdded": "Manager added successfully", "managerAdded": "Manager added successfully",
"managerRemoved": "Manager removed successfully" "managerRemoved": "Manager removed successfully",
"leaderChanged": "Leader has been changed"
} }

View File

@@ -246,6 +246,10 @@ function _getMembersForItem (type) {
addComputedStats = true; addComputedStats = true;
} }
} }
if (req.query.search) {
query['profile.name'] = {$regex: req.query.search};
}
} else if (type === 'group-invites') { } else if (type === 'group-invites') {
if (group.type === 'guild') { // eslint-disable-line no-lonely-if if (group.type === 'guild') { // eslint-disable-line no-lonely-if
query['invitations.guilds.id'] = group._id; query['invitations.guilds.id'] = group._id;