From 9a00779698ea077a5bac143c8cc457f267612042 Mon Sep 17 00:00:00 2001 From: Jon Lim Date: Mon, 5 Feb 2018 13:42:54 -0500 Subject: [PATCH] Adding support for loading more members for Groups (#9740) * Adding support for loading more members for Groups Addresses issue #9720 Member modal component was only loading the maximum limit for queries made to _getMembersForItem in /members, except with Challenges, which was able to display a "Load More" button to retrieve another set of users from the Challenge. Made a few changes to how the GET request was being made when querying for more members, added an easier way to know whether or not to display the Load More button, and extracted some of the actions that were too tightly coupled with the membersModal.vue. * Fixes for failing lint tests * Removing unnecessary async/await usage * Fixing party view in header section * Resolving missed conflict * Adding necessary data for View Party in index header for web client to load party members --- .../components/challenges/challengeDetail.vue | 19 ++++++- website/client/components/groups/group.vue | 39 +++++++++++++-- .../client/components/groups/membersModal.vue | 50 +++++++++++-------- website/client/components/header/index.vue | 10 ++-- website/client/store/actions/members.js | 26 +++++++--- website/server/controllers/api-v3/members.js | 3 +- 6 files changed, 110 insertions(+), 37 deletions(-) diff --git a/website/client/components/challenges/challengeDetail.vue b/website/client/components/challenges/challengeDetail.vue index efa05268ae..19ddd1f183 100644 --- a/website/client/components/challenges/challengeDetail.vue +++ b/website/client/components/challenges/challengeDetail.vue @@ -281,7 +281,7 @@ export default { }, async loadChallenge () { this.challenge = await this.$store.dispatch('challenges:getChallenge', {challengeId: this.searchId}); - this.members = await this.$store.dispatch('members:getChallengeMembers', {challengeId: this.searchId}); + this.members = await this.loadMembers({ challengeId: this.searchId, includeAllPublicFields: true }); let tasks = await this.$store.dispatch('tasks:getChallengeTasks', {challengeId: this.searchId}); this.tasksByType = { habit: [], @@ -293,6 +293,21 @@ export default { this.tasksByType[task.type].push(task); }); }, + + /** + * Method for loading members of a group, with optional parameters for + * modifying requests. + * + * @param {Object} payload Used for modifying requests for members + */ + loadMembers (payload = null) { + // Remove unnecessary data + if (payload && payload.groupId) { + delete payload.groupId; + } + return this.$store.dispatch('members:getChallengeMembers', payload); + }, + editTask (task) { this.taskFormPurpose = 'edit'; this.editingTask = cloneDeep(task); @@ -334,7 +349,9 @@ export default { this.$store.state.memberModalOptions.challengeId = this.challenge._id; this.$store.state.memberModalOptions.groupId = 'challenge'; // @TODO: change these terrible settings this.$store.state.memberModalOptions.group = this.group; + this.$store.state.memberModalOptions.memberCount = this.challenge.memberCount; this.$store.state.memberModalOptions.viewingMembers = this.members; + this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers; this.$root.$emit('bv::show::modal', 'members-modal'); }, async joinChallenge () { diff --git a/website/client/components/groups/group.vue b/website/client/components/groups/group.vue index 03df579d1d..d2e7b1468a 100644 --- a/website/client/components/groups/group.vue +++ b/website/client/components/groups/group.vue @@ -375,6 +375,7 @@ export default { silverGuildBadgeIcon, bronzeGuildBadgeIcon, }), + members: [], selectedQuest: {}, sections: { quest: true, @@ -456,14 +457,43 @@ export default { }, }, methods: { - load () { - this.fetchGuild(); - + acceptCommunityGuidelines () { + this.$store.dispatch('user:set', {'flags.communityGuidelinesAccepted': true}); + }, + async load () { + if (this.isParty) { + this.searchId = 'party'; + // @TODO: Set up from old client. Decide what we need and what we don't + // Check Desktop notifs + // Load invites + } + await this.fetchGuild(); + // Fetch group members on load + this.members = await this.loadMembers({ + groupId: this.group._id, + includeAllPublicFields: true, + }); this.$root.$on('updatedGroup', group => { let updatedGroup = extend(this.group, group); this.$set(this.group, updatedGroup); }); }, + + /** + * Method for loading members of a group, with optional parameters for + * modifying requests. + * + * @param {Object} payload Used for modifying requests for members + */ + loadMembers (payload = null) { + // Remove unnecessary data + if (payload && payload.challengeId) { + delete payload.challengeId; + } + + return this.$store.dispatch('members:getGroupMembers', payload); + }, + // @TODO: abstract autocomplete // https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a getCoord (e, text) { @@ -500,6 +530,9 @@ export default { showMemberModal () { this.$store.state.memberModalOptions.groupId = this.group._id; this.$store.state.memberModalOptions.group = this.group; + this.$store.state.memberModalOptions.memberCount = this.group.memberCount; + this.$store.state.memberModalOptions.viewingMembers = this.members; + this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers; this.$root.$emit('bv::show::modal', 'members-modal'); }, async sendMessage () { diff --git a/website/client/components/groups/membersModal.vue b/website/client/components/groups/membersModal.vue index 1b6c0cb1b7..eb0199fc03 100644 --- a/website/client/components/groups/membersModal.vue +++ b/website/client/components/groups/membersModal.vue @@ -48,7 +48,7 @@ div span.dropdown-icon-item .svg-icon.inline(v-html="icons.removeIcon") span.text {{$t('removeManager2')}} - .row(v-if='groupId === "challenge"') + .row(v-if='isLoadMoreAvailable') .col-12.text-center button.btn.btn-secondary(@click='loadMoreMembers()') {{ $t('loadMore') }} .row.gradient(v-if='members.length > 3') @@ -296,6 +296,11 @@ export default { isAdmin () { return Boolean(this.user.contributor.admin); }, + isLoadMoreAvailable () { + // Only available if the current length of `members` is less than the + // total size of the Group/Challenge + return this.members.length < this.$store.state.memberModalOptions.memberCount; + }, groupIsSubscribed () { return this.group.purchased.active; }, @@ -311,16 +316,6 @@ export default { sortedMembers () { let sortedMembers = this.members; - if (this.searchTerm) { - sortedMembers = sortedMembers.filter(member => { - return ( - member.profile.name - .toLowerCase() - .indexOf(this.searchTerm.toLowerCase()) !== -1 - ); - }); - } - if (!isEmpty(this.sortOption)) { // Use the memberlist filtered by searchTerm sortedMembers = orderBy(sortedMembers, [this.sortOption.param], [this.sortOption.order]); @@ -337,6 +332,15 @@ export default { group () { this.getMembers(); }, + // Watches `searchTerm` and if present, performs a `searchMembers` action + // and usual `getMembers` otherwise + searchTerm () { + if (this.searchTerm) { + this.searchMembers(this.searchTerm); + } else { + this.getMembers(); + } + }, }, methods: { sendMessage (member) { @@ -345,22 +349,24 @@ export default { userName: member.profile.name, }); }, + async searchMembers (searchTerm = '') { + this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({ + challengeId: this.challengeId, + groupId: this.groupId, + searchTerm, + includeAllPublicFields: true, + }); + }, async getMembers () { let groupId = this.groupId; - if (groupId && groupId !== 'challenge') { - let members = await this.$store.dispatch('members:getGroupMembers', { - groupId, - includeAllPublicFields: true, - }); - this.members = members; + if (groupId && groupId !== 'challenge') { let invites = await this.$store.dispatch('members:getGroupInvites', { groupId, includeAllPublicFields: true, }); this.invites = invites; } - if (this.$store.state.memberModalOptions.viewingMembers.length > 0) { this.members = this.$store.state.memberModalOptions.viewingMembers; } @@ -425,9 +431,11 @@ export default { const lastMember = this.members[this.members.length - 1]; if (!lastMember) return; - let newMembers = await this.$store.dispatch('members:getChallengeMembers', { - challengeId: this.challengeId, - lastMemberId: lastMember._id, + let newMembers = await this.$store.state.memberModalOptions.fetchMoreMembers({ + challengeId: this.challengeId, + groupId: this.groupId, + lastMemberId: lastMember._id, + includeAllPublicFields: true, }); this.members = this.members.concat(newMembers); diff --git a/website/client/components/header/index.vue b/website/client/components/header/index.vue index 3f3602b862..e679b2c5cf 100644 --- a/website/client/components/header/index.vue +++ b/website/client/components/header/index.vue @@ -160,9 +160,10 @@ export default { }, openPartyModal () { if (this.user.party._id) { + // Set the party details for the members-modal component this.$store.state.memberModalOptions.groupId = this.user.party._id; - // @TODO: do we need to fetch party? - // this.$store.state.memberModalOptions.group = this.$store.state.party; + this.$store.state.memberModalOptions.viewingMembers = this.partyMembers; + this.$store.state.memberModalOptions.group = this.user.party; this.$root.$emit('bv::show::modal', 'members-modal'); return; } @@ -174,9 +175,10 @@ export default { } }, }, - async created () { + created () { if (this.user.party && this.user.party._id) { - await this.getPartyMembers(true); + this.$store.state.memberModalOptions.groupId = this.user.party._id; + this.getPartyMembers(); } }, }; diff --git a/website/client/store/actions/members.js b/website/client/store/actions/members.js index 864b6d4d5b..aeff9ad9f6 100644 --- a/website/client/store/actions/members.js +++ b/website/client/store/actions/members.js @@ -7,15 +7,21 @@ let apiV3Prefix = '/api/v3'; export async function getGroupMembers (store, payload) { let url = `${apiV3Prefix}/groups/${payload.groupId}/members`; + const params = {}; + if (payload.includeAllPublicFields) { - url += '?includeAllPublicFields=true'; + params.includeAllPublicFields = true; + } + + if (payload.lastMemberId) { + params.lastId = payload.lastMemberId; } if (payload.searchTerm) { - url += `?search=${payload.searchTerm}`; + params.search = payload.searchTerm; } - let response = await axios.get(url); + let response = await axios.get(url, { params }); return response.data.data; } @@ -35,17 +41,23 @@ export async function getGroupInvites (store, payload) { } export async function getChallengeMembers (store, payload) { - let url = `${apiV3Prefix}/challenges/${payload.challengeId}/members?includeAllPublicFields=true`; + let url = `${apiV3Prefix}/challenges/${payload.challengeId}/members`; + + const params = {}; + + if (payload.includeAllPublicFields) { + params.includeAllPublicFields = true; + } if (payload.lastMemberId) { - url += `&lastId=${payload.lastMemberId}`; + params.lastId = payload.lastMemberId; } if (payload.searchTerm) { - url += `&search=${payload.searchTerm}`; + params.search = payload.searchTerm; } - let response = await axios.get(url); + let response = await axios.get(url, { params }); return response.data.data; } diff --git a/website/server/controllers/api-v3/members.js b/website/server/controllers/api-v3/members.js index ae9201831f..b9e390891e 100644 --- a/website/server/controllers/api-v3/members.js +++ b/website/server/controllers/api-v3/members.js @@ -248,7 +248,8 @@ function _getMembersForItem (type) { } if (req.query.search) { - query['profile.name'] = {$regex: req.query.search}; + // Creates a RegExp expression when querying for profile.name + query['profile.name'] = { $regex: new RegExp(req.query.search, 'i') }; } } else if (type === 'group-invites') { if (group.type === 'guild') { // eslint-disable-line no-lonely-if