mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
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:
@@ -161,4 +161,19 @@ describe('GET /groups/:groupId/members', () => {
|
||||
let resIds = res.concat(res2).map(member => member._id);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,10 +33,7 @@
|
||||
.col-7.offset-5
|
||||
span.view-progress
|
||||
strong {{ $t('viewProgressOf') }}
|
||||
b-dropdown.create-dropdown(text="Select a Participant")
|
||||
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 }}
|
||||
member-search-dropdown(:text="$t('selectParticipant')", :members='members', :challengeId='challengeId', @member-selected='openMemberProgressModal')
|
||||
span(v-if='isLeader || isAdmin')
|
||||
b-dropdown.create-dropdown(:text="$t('addTaskToChallenge')", :variant="'success'")
|
||||
b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)")
|
||||
@@ -51,7 +48,6 @@
|
||||
v-on:taskEdited='taskEdited',
|
||||
@taskDestroyed='taskDestroyed'
|
||||
)
|
||||
|
||||
.row
|
||||
task-column.col-12.col-sm-6(
|
||||
v-for="column in columns",
|
||||
@@ -185,6 +181,7 @@ import omit from 'lodash/omit';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
|
||||
import closeChallengeModal from './closeChallengeModal';
|
||||
import Column from '../tasks/column';
|
||||
import TaskModal from '../tasks/taskModal';
|
||||
@@ -211,6 +208,7 @@ export default {
|
||||
leaveChallengeModal,
|
||||
challengeModal,
|
||||
challengeMemberProgressModal,
|
||||
memberSearchDropdown,
|
||||
TaskColumn: Column,
|
||||
TaskModal,
|
||||
},
|
||||
@@ -388,8 +386,8 @@ export default {
|
||||
updatedChallenge (eventData) {
|
||||
Object.assign(this.challenge, eventData.challenge);
|
||||
},
|
||||
openMemberProgressModal (memberId) {
|
||||
this.progressMemberId = memberId;
|
||||
openMemberProgressModal (member) {
|
||||
this.progressMemberId = member._id;
|
||||
this.$root.$emit('bv::show::modal', 'challenge-member-modal');
|
||||
},
|
||||
async exportChallengeCsv () {
|
||||
|
||||
@@ -10,10 +10,7 @@ div
|
||||
.col-12
|
||||
strong(v-once) {{$t('selectChallengeWinnersDescription')}}
|
||||
.col-12
|
||||
b-dropdown.create-dropdown(:text="winnerText")
|
||||
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 }}
|
||||
member-search-dropdown(:text='winnerText', :members='members', :challengeId='challengeId', @member-selected='selectMember')
|
||||
.col-12
|
||||
button.btn.btn-primary(v-once, @click='closeChallenge') {{$t('awardWinners')}}
|
||||
.col-12
|
||||
@@ -74,16 +71,16 @@ div
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import challengeMemberSearchMixin from 'client/mixins/challengeMemberSearch';
|
||||
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
|
||||
|
||||
export default {
|
||||
props: ['challengeId', 'members'],
|
||||
mixins: [challengeMemberSearchMixin],
|
||||
components: {
|
||||
memberSearchDropdown,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
winner: {},
|
||||
searchTerm: '',
|
||||
memberResults: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
.form-group(v-if='workingGroup.id && members.length > 0')
|
||||
label
|
||||
strong(v-once) {{$t('guildOrPartyLeader')}} *
|
||||
select.form-control(v-model="workingGroup.newLeader")
|
||||
option(v-for='potentialLeader in potentialLeaders', :value="potentialLeader._id") {{ potentialLeader.name }}
|
||||
|
||||
group-member-search-dropdown(:text="currentLeader", :members='members', :groupId='workingGroup.id', @member-selected='selectNewLeader')
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('privacySettings')}} *
|
||||
@@ -170,6 +168,7 @@
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||
import groupMemberSearchDropdown from 'client/components/members/groupMemberSearchDropdown';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import informationIcon from 'assets/svg/information.svg';
|
||||
@@ -185,6 +184,7 @@ import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../common/script/constants';
|
||||
export default {
|
||||
components: {
|
||||
toggleSwitch,
|
||||
groupMemberSearchDropdown,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
@@ -307,16 +307,12 @@ export default {
|
||||
isParty () {
|
||||
return this.workingGroup.type === 'party';
|
||||
},
|
||||
potentialLeaders () {
|
||||
let leaders = [{ _id: this.user._id, name: this.user.profile.name }];
|
||||
// @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)
|
||||
let i = 0;
|
||||
while (this.members[i]) {
|
||||
let memb = this.members[i];
|
||||
i++;
|
||||
if (memb._id !== this.user._id) leaders.push({_id: memb._id, name: memb.profile.name});
|
||||
}
|
||||
return leaders;
|
||||
currentLeader () {
|
||||
const currentLeader = this.members.find(member => {
|
||||
return member._id === this.workingGroup.newLeader;
|
||||
});
|
||||
const currentLeaderName = currentLeader.profile ? currentLeader.profile.name : '';
|
||||
return currentLeaderName;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -356,6 +352,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectNewLeader (member) {
|
||||
this.workingGroup.newLeader = member._id;
|
||||
},
|
||||
async getMembers () {
|
||||
if (!this.workingGroup.id) return;
|
||||
let members = await this.$store.dispatch('members:getGroupMembers', {
|
||||
|
||||
@@ -36,7 +36,7 @@ div
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.messageIcon")
|
||||
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
|
||||
.svg-icon.inline(v-html="icons.starIcon")
|
||||
span.text {{$t('promoteToLeader')}}
|
||||
@@ -290,6 +290,9 @@ export default {
|
||||
if (!this.group || !this.group.leader) return false;
|
||||
return this.user._id === this.group.leader || this.user._id === this.group.leader._id;
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
},
|
||||
groupIsSubscribed () {
|
||||
return this.group.purchased.active;
|
||||
},
|
||||
@@ -440,10 +443,15 @@ export default {
|
||||
});
|
||||
this.viewMembers();
|
||||
},
|
||||
async promoteToLeader (memberId) {
|
||||
async promoteToLeader (member) {
|
||||
let groupData = Object.assign({}, this.group);
|
||||
groupData.leader = memberId;
|
||||
|
||||
groupData.leader = member._id;
|
||||
await this.$store.dispatch('guilds:update', {group: groupData});
|
||||
|
||||
alert(this.$t('leaderChanged'));
|
||||
|
||||
groupData.leader = member;
|
||||
this.$root.$emit('updatedGroup', groupData);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
42
website/client/components/members/memberSearchDropdown.vue
Normal file
42
website/client/components/members/memberSearchDropdown.vue
Normal 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>
|
||||
@@ -6,9 +6,15 @@ let apiV3Prefix = '/api/v3';
|
||||
|
||||
export async function getGroupMembers (store, payload) {
|
||||
let url = `${apiV3Prefix}/groups/${payload.groupId}/members`;
|
||||
|
||||
if (payload.includeAllPublicFields) {
|
||||
url += '?includeAllPublicFields=true';
|
||||
}
|
||||
|
||||
if (payload.searchTerm) {
|
||||
url += `?search=${payload.searchTerm}`;
|
||||
}
|
||||
|
||||
let response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -130,5 +130,6 @@
|
||||
"categoiresRequired": "One or more categories must be selected",
|
||||
"viewProgressOf": "View Progress Of",
|
||||
"selectMember": "Select Member",
|
||||
"confirmKeepChallengeTasks": "Do you want to keep challenge tasks?"
|
||||
"confirmKeepChallengeTasks": "Do you want to keep challenge tasks?",
|
||||
"selectParticipant": "Select a Participant"
|
||||
}
|
||||
|
||||
@@ -413,5 +413,6 @@
|
||||
"groupBilling": "Group Billing",
|
||||
"wouldYouParticipate": "Would you like to participate?",
|
||||
"managerAdded": "Manager added successfully",
|
||||
"managerRemoved": "Manager removed successfully"
|
||||
"managerRemoved": "Manager removed successfully",
|
||||
"leaderChanged": "Leader has been changed"
|
||||
}
|
||||
|
||||
@@ -246,6 +246,10 @@ function _getMembersForItem (type) {
|
||||
addComputedStats = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.query.search) {
|
||||
query['profile.name'] = {$regex: req.query.search};
|
||||
}
|
||||
} else if (type === 'group-invites') {
|
||||
if (group.type === 'guild') { // eslint-disable-line no-lonely-if
|
||||
query['invitations.guilds.id'] = group._id;
|
||||
|
||||
Reference in New Issue
Block a user