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);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user