mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-14 11:36:45 +00:00
712 lines
20 KiB
Vue
712 lines
20 KiB
Vue
<template>
|
||
<!-- @TODO: Move this to a member directory-->
|
||
<div>
|
||
<remove-member-modal
|
||
:member-to-remove="memberToRemove"
|
||
:group-id="groupId"
|
||
@member-removed="memberRemoved"
|
||
/>
|
||
<b-modal
|
||
id="members-modal"
|
||
:title="$t('createGuild')"
|
||
size="md"
|
||
:hide-footer="true"
|
||
>
|
||
<div
|
||
slot="modal-header"
|
||
class="header-wrap"
|
||
>
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<h1 v-once>
|
||
{{ $t('members') }}
|
||
</h1>
|
||
</div>
|
||
<div class="col-6">
|
||
<button
|
||
class="close"
|
||
type="button"
|
||
aria-label="Close"
|
||
@click="close()"
|
||
>
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="row d-flex align-items-center">
|
||
<div class="col-4">
|
||
<input
|
||
v-model="searchTerm"
|
||
class="form-control input-search"
|
||
type="text"
|
||
:placeholder="$t('search')"
|
||
>
|
||
</div>
|
||
<div class="col-5">
|
||
<select-list
|
||
:items="sortOptions"
|
||
:hide-icon="false"
|
||
:inline-dropdown="false"
|
||
:direct-select="true"
|
||
:value="optionEntryBySelectedValue"
|
||
key-prop="value"
|
||
@select="changeSortOption($event)"
|
||
>
|
||
<template #item="{ item }">
|
||
<span
|
||
v-if="item"
|
||
class="label"
|
||
>{{ item.text }}</span>
|
||
</template>
|
||
</select-list>
|
||
</div>
|
||
<div class="col-3">
|
||
<select-list
|
||
:items="sortDirections"
|
||
:value="directionEntryBySelectedValue"
|
||
:hide-icon="false"
|
||
:inline-dropdown="false"
|
||
:direct-select="true"
|
||
key-prop="value"
|
||
@select="changeSortDirection($event)"
|
||
>
|
||
<template #item="{ item }">
|
||
<span
|
||
v-if="item"
|
||
class="label"
|
||
>{{ item.text }}</span>
|
||
</template>
|
||
</select-list>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="sortDirty && group.type === 'party'"
|
||
class="row apply-options d-flex justify-content-center"
|
||
>
|
||
<a @click="applySortOptions()">{{ $t('applySortToHeader') }}</a>
|
||
</div>
|
||
<div
|
||
v-if="invites.length > 0"
|
||
class="row"
|
||
>
|
||
<div class="col-6 offset-3 nav mt-2 mb-3">
|
||
<div
|
||
class="nav-item"
|
||
:class="{active: selectedPage === 'members'}"
|
||
@click="viewMembers()"
|
||
>
|
||
{{ $t('members') }}
|
||
</div>
|
||
<div
|
||
class="nav-item"
|
||
:class="{active: selectedPage === 'invites'}"
|
||
@click="viewInvites()"
|
||
>
|
||
{{ $t('invites') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<loading-gryphon v-if="loading" />
|
||
<div
|
||
v-if="selectedPage === 'members' && !loading"
|
||
:class="{'mt-1': invites.length === 0}"
|
||
>
|
||
<div
|
||
v-for="(member, index) in sortedMembers"
|
||
:key="member._id"
|
||
class="row"
|
||
>
|
||
<div class="col-11 pl-0">
|
||
<member-details
|
||
:member="member"
|
||
:class-badge-position="'next-to-name'"
|
||
class="ml-4"
|
||
/>
|
||
</div>
|
||
<div class="col-1 actions">
|
||
<b-dropdown right="right">
|
||
<div
|
||
slot="button-content"
|
||
class="svg-icon inline dots pt-1"
|
||
v-html="icons.dots"
|
||
></div>
|
||
<b-dropdown-item @click="sendMessage(member)">
|
||
<span class="dropdown-icon-item">
|
||
<div
|
||
class="svg-icon inline"
|
||
v-html="icons.messageIcon"
|
||
></div>
|
||
<span class="text">{{ $t('sendMessage') }}</span>
|
||
</span>
|
||
</b-dropdown-item>
|
||
<b-dropdown-item
|
||
v-if="shouldShowLeaderFunctions(member._id)"
|
||
@click="promoteToLeader(member)"
|
||
>
|
||
<span class="dropdown-icon-item">
|
||
<div
|
||
class="svg-icon inline"
|
||
v-html="icons.starIcon"
|
||
></div>
|
||
<span class="text">{{ $t('promoteToLeader') }}</span>
|
||
</span>
|
||
</b-dropdown-item>
|
||
<b-dropdown-item
|
||
v-if="shouldShowAddManager(member._id)"
|
||
@click="addManager(member._id)"
|
||
>
|
||
<span class="dropdown-icon-item">
|
||
<div
|
||
class="svg-icon inline"
|
||
v-html="icons.starIcon"
|
||
></div>
|
||
<span class="text">{{ $t('addManager') }}</span>
|
||
</span>
|
||
</b-dropdown-item>
|
||
<b-dropdown-item
|
||
v-if="shouldShowRemoveManager(member._id)"
|
||
@click="removeManager(member._id)"
|
||
>
|
||
<span class="dropdown-icon-item">
|
||
<div
|
||
class="svg-icon inline block-icon"
|
||
v-html="icons.blockIcon"
|
||
></div>
|
||
<span class="text">{{ $t('removeManager2') }}</span>
|
||
</span>
|
||
</b-dropdown-item>
|
||
<b-dropdown-item
|
||
v-if="challengeId"
|
||
@click="viewProgress(member)"
|
||
>
|
||
<span class="dropdown-icon-item">
|
||
<span class="text">{{ $t('viewProgress') }}</span>
|
||
</span>
|
||
</b-dropdown-item>
|
||
<b-dropdown-item
|
||
v-if="shouldShowLeaderFunctions(member._id)"
|
||
@click="removeMember(member, index)"
|
||
>
|
||
<span class="dropdown-icon-item">
|
||
<div
|
||
class="svg-icon inline block-icon"
|
||
v-html="icons.blockIcon"
|
||
></div>
|
||
<span class="text">{{ $t('removeMember') }}</span>
|
||
</span>
|
||
</b-dropdown-item>
|
||
</b-dropdown>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="isLoadMoreAvailable"
|
||
class="row"
|
||
>
|
||
<div class="col-12 text-center">
|
||
<button
|
||
class="btn btn-secondary"
|
||
@click="loadMoreMembers()"
|
||
>
|
||
{{ $t('loadMore') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="members.length > 3"
|
||
class="row gradient"
|
||
></div>
|
||
</div>
|
||
<div v-if="selectedPage === 'invites' && !loading">
|
||
<div
|
||
v-for="(member, index) in invites"
|
||
:key="member._id"
|
||
class="row"
|
||
>
|
||
<div class="col-11 pl-0">
|
||
<member-details :member="member" />
|
||
</div>
|
||
<div class="col-1 actions">
|
||
<b-dropdown right="right">
|
||
<div
|
||
slot="button-content"
|
||
class="svg-icon inline dots"
|
||
v-html="icons.dots"
|
||
></div>
|
||
<b-dropdown-item
|
||
v-if="isLeader"
|
||
@click="removeInvite(member, index)"
|
||
>
|
||
<span class="dropdown-icon-item">
|
||
<div
|
||
v-if="isLeader"
|
||
class="svg-icon inline block-icon"
|
||
v-html="icons.blockIcon"
|
||
></div>
|
||
<span class="text">{{ $t('removeInvite') }}</span>
|
||
</span>
|
||
</b-dropdown-item>
|
||
</b-dropdown>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</b-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang='scss'>
|
||
#members-modal {
|
||
.modal-header {
|
||
background-color: #edecee;
|
||
border-radius: 8px 8px 0 0;
|
||
box-shadow: 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||
}
|
||
|
||
.small-text, .character-name {
|
||
color: #878190;
|
||
}
|
||
|
||
.modal-body {
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
padding-bottom: 0;
|
||
padding-top: 0;
|
||
}
|
||
|
||
.member-details {
|
||
margin: 0;
|
||
}
|
||
|
||
.actions .dropdown-toggle::after {
|
||
content: none !important;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang='scss' scoped>
|
||
@import '@/assets/scss/colors.scss';
|
||
|
||
.apply-options {
|
||
padding: 1em;
|
||
margin: 0;
|
||
background-color: #f9f9f9;
|
||
color: #2995cd;
|
||
}
|
||
|
||
.header-wrap {
|
||
width: 100%;
|
||
}
|
||
|
||
.form-control {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
h1 {
|
||
color: #4f2a93;
|
||
}
|
||
|
||
.actions {
|
||
.b-dropdown {
|
||
position: absolute;
|
||
right: 24px;
|
||
top: 8px;
|
||
}
|
||
.dots {
|
||
height: 16px;
|
||
width: 4px;
|
||
}
|
||
}
|
||
|
||
#members-modal_modal_body {
|
||
padding: 0;
|
||
max-height: 450px;
|
||
|
||
.col-8 {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.member-details {
|
||
margin: 0;
|
||
}
|
||
|
||
.member-stats {
|
||
width: 382px;
|
||
height: 147px;
|
||
}
|
||
|
||
.gradient {
|
||
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0), #ffffff);
|
||
height: 50px;
|
||
width: 100%;
|
||
position: absolute;
|
||
bottom: 0px;
|
||
margin-left: -15px;
|
||
}
|
||
}
|
||
|
||
.dropdown-icon-item .svg-icon {
|
||
width: 20px;
|
||
}
|
||
|
||
.nav {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.nav-item {
|
||
display: inline-block;
|
||
font-size: 16px;
|
||
margin: 0 auto;
|
||
padding: .5em;
|
||
color: #878190;
|
||
}
|
||
|
||
.nav-item:hover, .nav-item.active {
|
||
color: #4f2a93;
|
||
border-bottom: 2px solid #4f2a93;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.block-icon {
|
||
color: $gray-200;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
import orderBy from 'lodash/orderBy';
|
||
import isEmpty from 'lodash/isEmpty';
|
||
|
||
import removeMemberModal from '@/components/members/removeMemberModal';
|
||
import loadingGryphon from '@/components/ui/loadingGryphon';
|
||
import MemberDetails from '../memberDetails';
|
||
import blockIcon from '@/assets/svg/block.svg?raw';
|
||
import messageIcon from '@/assets/members/message.svg?raw';
|
||
import starIcon from '@/assets/members/star.svg?raw';
|
||
import dots from '@/assets/svg/dots.svg?raw';
|
||
import SelectList from '@/components/ui/selectList';
|
||
import { PAGES } from '@/libs/consts';
|
||
import { userStateMixin } from '../../mixins/userState';
|
||
|
||
export default {
|
||
components: {
|
||
SelectList,
|
||
MemberDetails,
|
||
removeMemberModal,
|
||
loadingGryphon,
|
||
},
|
||
mixins: [userStateMixin],
|
||
props: ['hideBadge'],
|
||
data () {
|
||
return {
|
||
sortOption: {
|
||
// default sort options
|
||
value: 'stats.class',
|
||
direction: 'asc',
|
||
},
|
||
sortDirty: false,
|
||
selectedPage: 'members',
|
||
members: [],
|
||
invites: [],
|
||
memberToRemove: {},
|
||
sortOptions: [
|
||
{
|
||
value: 'stats.class',
|
||
text: this.$t('sortClass'),
|
||
},
|
||
{
|
||
value: 'preferences.background',
|
||
text: this.$t('sortBackground'),
|
||
},
|
||
{
|
||
value: 'auth.timestamps.created',
|
||
text: this.$t('sortDateJoined'),
|
||
},
|
||
{
|
||
value: 'auth.timestamps.loggedin',
|
||
text: this.$t('sortLogin'),
|
||
},
|
||
{
|
||
value: 'stats.lvl',
|
||
text: this.$t('sortLevel'),
|
||
},
|
||
{
|
||
value: 'profile.name',
|
||
text: this.$t('sortName'),
|
||
},
|
||
{
|
||
value: 'contributor.level',
|
||
text: this.$t('sortTier'),
|
||
},
|
||
],
|
||
sortDirections: [
|
||
{
|
||
value: 'asc',
|
||
text: this.$t('ascendingAbbrev'),
|
||
},
|
||
{
|
||
value: 'desc',
|
||
text: this.$t('descendingAbbrev'),
|
||
},
|
||
],
|
||
searchTerm: '',
|
||
icons: Object.freeze({
|
||
blockIcon,
|
||
messageIcon,
|
||
starIcon,
|
||
dots,
|
||
}),
|
||
userIdToMessage: '',
|
||
};
|
||
},
|
||
computed: {
|
||
isLeader () {
|
||
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.hasPermission(this.user, 'moderator'));
|
||
},
|
||
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 && this.group.purchased.active;
|
||
},
|
||
group () {
|
||
return this.$store.state.memberModalOptions.group;
|
||
},
|
||
groupId () {
|
||
return this.$store.state.memberModalOptions.groupId || this.group._id;
|
||
},
|
||
challengeId () {
|
||
return this.$store.state.memberModalOptions.challengeId;
|
||
},
|
||
loading () {
|
||
return this.$store.state.memberModalOptions.loading;
|
||
},
|
||
sortedMembers () {
|
||
let sortedMembers = this.members.slice(); // shallow clone to avoid infinite loop
|
||
|
||
if (!isEmpty(this.sortOption)) {
|
||
// Use the memberlist filtered by searchTerm
|
||
if (this.sortOption.value === 'profile.name') {
|
||
// If members are to be sorted by name, use localeCompare for case-
|
||
// insensitive sort
|
||
sortedMembers.sort(
|
||
(a, b) => {
|
||
if (this.sortOption.direction === 'desc') {
|
||
return b.profile.name.localeCompare(a.profile.name);
|
||
}
|
||
|
||
return a.profile.name.localeCompare(b.profile.name);
|
||
},
|
||
);
|
||
} else {
|
||
sortedMembers = orderBy(
|
||
sortedMembers,
|
||
[this.sortOption.value],
|
||
[this.sortOption.direction],
|
||
);
|
||
}
|
||
}
|
||
|
||
return sortedMembers;
|
||
},
|
||
optionEntryBySelectedValue () {
|
||
return this.sortOptions.find(o => o.value === this.sortOption.value);
|
||
},
|
||
directionEntryBySelectedValue () {
|
||
return this.sortDirections.find(o => o.value === this.sortOption.direction);
|
||
},
|
||
},
|
||
watch: {
|
||
// Watches `searchTerm` and if present, performs a `searchMembers` action
|
||
// and usual `getMembers` otherwise
|
||
searchTerm () {
|
||
if (this.searchTerm) {
|
||
this.searchMembers(this.searchTerm);
|
||
} else {
|
||
this.getMembers();
|
||
}
|
||
},
|
||
},
|
||
mounted () {
|
||
this.$root.$on('habitica:show-member-modal', data => {
|
||
// @TODO: Remove store
|
||
this.$store.state.memberModalOptions.challengeId = data.challengeId;
|
||
this.$store.state.memberModalOptions.groupId = data.groupId;
|
||
this.$store.state.memberModalOptions.group = data.group;
|
||
this.$store.state.memberModalOptions.memberCount = data.memberCount;
|
||
this.$store.state.memberModalOptions.viewingMembers = data.viewingMembers;
|
||
this.$store.state.memberModalOptions.fetchMoreMembers = data.fetchMoreMembers;
|
||
this.$root.$emit('bv::show::modal', 'members-modal');
|
||
this.getMembers();
|
||
});
|
||
},
|
||
beforeDestroy () {
|
||
this.$root.$off('habitica:show-member-modal');
|
||
},
|
||
methods: {
|
||
sendMessage (member) {
|
||
this.$store.dispatch('user:newPrivateMessageTo', {
|
||
member,
|
||
});
|
||
|
||
this.$root.$emit('bv::hide::modal', 'members-modal');
|
||
this.$router.push(PAGES.PRIVATE_MESSAGES);
|
||
},
|
||
async searchMembers (searchTerm = '') {
|
||
this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({
|
||
challengeId: this.challengeId,
|
||
groupId: this.groupId,
|
||
searchTerm,
|
||
includeAllPublicFields: true,
|
||
});
|
||
},
|
||
async getMembers () {
|
||
this.members = this.$store.state.memberModalOptions.viewingMembers;
|
||
|
||
await this.searchMembers('');
|
||
|
||
const { groupId } = this;
|
||
if (groupId && groupId !== 'challenge') {
|
||
const invites = await this.$store.dispatch('members:getGroupInvites', {
|
||
groupId,
|
||
includeAllPublicFields: true,
|
||
});
|
||
if (this.selectedPage === 'invites' && invites.length === 0) this.viewMembers();
|
||
this.invites = invites;
|
||
}
|
||
},
|
||
async clickMember (uid, forceShow) {
|
||
const user = this.$store.state.user.data;
|
||
|
||
if (user._id === uid && !forceShow) {
|
||
if (this.$route.name === 'tasks') {
|
||
this.$route.router.go('options.profile.avatar');
|
||
return;
|
||
}
|
||
|
||
this.$route.router.go('tasks');
|
||
return;
|
||
}
|
||
|
||
await this.$store.dispatch('members:selectMember', {
|
||
memberId: uid,
|
||
});
|
||
|
||
this.$root.$emit('bv::show::modal', 'members-modal');
|
||
},
|
||
async removeMember (member, index) {
|
||
this.memberToRemove = member;
|
||
this.memberToRemove.index = index;
|
||
this.$root.$emit('bv::show::modal', 'remove-member');
|
||
},
|
||
memberRemoved () {
|
||
this.members.splice(this.memberToRemove.index, 1);
|
||
this.group.memberCount -= 1;
|
||
this.memberToRemove = {};
|
||
},
|
||
async quickReply (uid) {
|
||
this.memberToReply = uid;
|
||
await this.$store.dispatch('members:selectMember', {
|
||
memberId: uid,
|
||
});
|
||
this.$root.$emit('bv::show::modal', 'private-message'); // MemberModalCtrl
|
||
},
|
||
async addManager (memberId) {
|
||
await this.$store.dispatch('guilds:addManager', {
|
||
groupId: this.groupId,
|
||
memberId,
|
||
});
|
||
window.alert(this.$t('managerAdded')); // eslint-disable-line no-alert
|
||
},
|
||
async removeManager (memberId) {
|
||
await this.$store.dispatch('guilds:removeManager', {
|
||
groupId: this.groupId,
|
||
memberId,
|
||
});
|
||
window.alert(this.$t('managerRemoved')); // eslint-disable-line no-alert
|
||
},
|
||
close () {
|
||
this.$root.$emit('bv::hide::modal', 'members-modal');
|
||
},
|
||
changeSortOption (e) {
|
||
this.sortOption.value = e.value;
|
||
this.sort();
|
||
},
|
||
changeSortDirection (e) {
|
||
this.sortOption.direction = e.value;
|
||
this.sort();
|
||
},
|
||
sort () {
|
||
this.sortDirty = true;
|
||
this.members = orderBy(this.members, [this.sortOption.value], [this.sortOption.direction]);
|
||
},
|
||
async applySortOptions () {
|
||
const settings = {
|
||
'party.order': this.sortOption.value,
|
||
'party.orderAscending': this.sortOption.direction,
|
||
};
|
||
await this.$store.dispatch('user:set', settings);
|
||
this.sortDirty = false;
|
||
},
|
||
async loadMoreMembers () {
|
||
const lastMember = this.members[this.members.length - 1];
|
||
if (!lastMember) return;
|
||
|
||
const newMembers = await this.$store.state.memberModalOptions.fetchMoreMembers({
|
||
challengeId: this.challengeId,
|
||
groupId: this.groupId,
|
||
lastMemberId: lastMember._id,
|
||
includeAllPublicFields: true,
|
||
});
|
||
|
||
this.members = this.members.concat(newMembers);
|
||
},
|
||
viewMembers () {
|
||
this.selectedPage = 'members';
|
||
},
|
||
viewInvites () {
|
||
this.selectedPage = 'invites';
|
||
},
|
||
async removeInvite (member, index) {
|
||
this.invites.splice(index, 1);
|
||
await this.$store.dispatch('members:removeMember', {
|
||
memberId: member._id,
|
||
groupId: this.groupId,
|
||
});
|
||
if (this.invites.length === 0) this.viewMembers();
|
||
},
|
||
async promoteToLeader (member) {
|
||
const groupData = { ...this.group };
|
||
|
||
groupData.leader = member._id;
|
||
await this.$store.dispatch('guilds:update', { group: groupData });
|
||
|
||
window.alert(this.$t('leaderChanged')); // eslint-disable-line no-alert
|
||
|
||
groupData.leader = member;
|
||
this.$root.$emit('updatedGroup', groupData);
|
||
},
|
||
viewProgress (member) {
|
||
this.$root.$emit('habitica:challenge:member-progress', {
|
||
progressMemberId: member._id,
|
||
});
|
||
},
|
||
shouldShowAddManager (memberId) {
|
||
if (!this.isLeader && !this.isAdmin) return false;
|
||
if (memberId === this.group.leader || memberId === this.group.leader._id) return false;
|
||
return this.groupIsSubscribed && !(this.group.managers && this.group.managers[memberId]);
|
||
},
|
||
shouldShowRemoveManager (memberId) {
|
||
if (!this.isLeader && !this.isAdmin) return false;
|
||
return this.group.managers && this.group.managers[memberId];
|
||
},
|
||
shouldShowLeaderFunctions (memberId) {
|
||
return !this.challengeId && (this.isLeader || this.isAdmin) && this.user._id !== memberId;
|
||
},
|
||
},
|
||
};
|
||
</script>
|