Files
hoppscotch/packages/hoppscotch-sh-admin/src/components/teams/Members.vue
2023-06-21 00:09:16 +05:30

352 lines
9.9 KiB
Vue

<template>
<div class="flex flex-col">
<div class="flex flex-col">
<div class="flex">
<HoppButtonPrimary
:icon="IconUserPlus"
:label="t('teams.add_members')"
filled
@click="showInvite = !showInvite"
/>
</div>
<div class="border rounded border-divider my-8">
<HoppSmartPlaceholder
v-if="team?.teamMembers?.length === 0"
text="No members in this team. Add members to this team to collaborate"
>
<HoppButtonSecondary
:icon="IconUserPlus"
:label="t('teams.add_members')"
@click="
() => {
showInvite = !showInvite;
}
"
/>
</HoppSmartPlaceholder>
<div v-else class="divide-y divide-dividerLight">
<div
v-for="(member, index) in membersList"
:key="`member-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
class="flex flex-1 px-4 py-3 bg-transparent"
placeholder="Email"
:name="'param' + index"
:value="member.email"
readonly
/>
<span>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions![index].focus()"
>
<span class="relative">
<input
class="flex flex-1 px-4 py-3 bg-transparent cursor-pointer"
placeholder="Permissions"
:name="'value' + index"
:value="member.role"
readonly
/>
<span
class="absolute right-4 top-1/2 transform !-translate-y-1/2"
>
<IconChevronDown />
</span>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
label="OWNER"
:icon="
member.role === 'OWNER' ? IconCircleDot : IconCircle
"
:active="member.role === 'OWNER'"
@click="
() => {
updateMemberRole(member.userID, TeamMemberRole.Owner);
hide();
}
"
/>
<HoppSmartItem
label="EDITOR"
:icon="
member.role === 'EDITOR' ? IconCircleDot : IconCircle
"
:active="member.role === 'EDITOR'"
@click="
() => {
updateMemberRole(
member.userID,
TeamMemberRole.Editor
);
hide();
}
"
/>
<HoppSmartItem
label="VIEWER"
:icon="
member.role === 'VIEWER' ? IconCircleDot : IconCircle
"
:active="member.role === 'VIEWER'"
@click="
() => {
updateMemberRole(
member.userID,
TeamMemberRole.Viewer
);
hide();
}
"
/>
</div>
</template>
</tippy>
</span>
<div class="flex">
<HoppButtonSecondary
id="member"
v-tippy="{ theme: 'tooltip' }"
:title="t('teams.remove')"
:icon="IconUserMinus"
color="red"
:loading="isLoadingIndex === index"
@click="removeExistingTeamMember(member.userID, index)"
/>
</div>
</div>
</div>
</div>
<div v-if="!fetching && !team" class="flex flex-col items-center">
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ t('teams.error') }}
</div>
</div>
<div class="flex">
<HoppButtonPrimary
:label="t('teams.save')"
outline
@click="saveUpdatedTeam"
/>
</div>
<TeamsInvite
:show="showInvite"
:editingTeamID="route.params.id.toString()"
@member="updateMembers"
@hide-modal="
() => {
showInvite = false;
}
"
/>
</div>
</template>
<script setup lang="ts">
import IconCircleDot from '~icons/lucide/circle-dot';
import IconCircle from '~icons/lucide/circle';
import IconUserPlus from '~icons/lucide/user-plus';
import IconUserMinus from '~icons/lucide/user-minus';
import IconChevronDown from '~icons/lucide/chevron-down';
import { useClientHandle, useMutation } from '@urql/vue';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useToast } from '~/composables/toast';
import {
ChangeUserRoleInTeamByAdminDocument,
TeamInfoDocument,
TeamMemberRole,
RemoveUserFromTeamByAdminDocument,
TeamInfoQuery,
} from '../../helpers/backend/graphql';
import { HoppButtonPrimary, HoppButtonSecondary } from '@hoppscotch/ui';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
const emit = defineEmits<{
(e: 'update-team'): void;
}>();
// Used to Invoke the Invite Members Modal
const showInvite = ref(false);
// Get Team Details
const team = ref<TeamInfoQuery['admin']['teamInfo'] | undefined>();
const fetching = ref(true);
const route = useRoute();
const { client } = useClientHandle();
const getTeamInfo = async () => {
fetching.value = true;
const result = await client
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
.toPromise();
if (result.error) {
return toast.error(`${t('teams.load_info_error')}`);
}
if (result.data?.admin.teamInfo) {
team.value = result.data.admin.teamInfo;
}
fetching.value = false;
};
onMounted(async () => await getTeamInfo());
onUnmounted(() => emit('update-team'));
// Update members tab after a change in the members list or member roles
const updateMembers = () => {
getTeamInfo();
emit('update-team');
};
// Template refs
const tippyActions = ref<any | null>(null);
const roleUpdates = ref<
{
userID: string;
role: TeamMemberRole;
}[]
>([]);
watch(
() => team.value,
(teamDetails) => {
const members = teamDetails?.teamMembers ?? [];
// Remove deleted members
roleUpdates.value = roleUpdates.value.filter(
(update) =>
members.findIndex(
(y: { user: { uid: string } }) => y.user.uid === update.userID
) !== -1
);
}
);
// Update the role of the member selected in the UI
const updateMemberRole = (userID: string, role: TeamMemberRole) => {
const updateIndex = roleUpdates.value.findIndex(
(item) => item.userID === userID
);
if (updateIndex !== -1) {
// Role Update exists
roleUpdates.value[updateIndex].role = role;
} else {
// Role Update does not exist
roleUpdates.value.push({
userID,
role,
});
}
};
// Obtain the list of members in the team
const membersList = computed(() => {
if (!team.value) return [];
const members = (team.value.teamMembers ?? []).map((member) => {
const updatedRole = roleUpdates.value.find(
(update) => update.userID === member.user.uid
);
return {
userID: member.user.uid,
email: member.user.email!,
role: updatedRole?.role ?? member.role,
};
});
return members;
});
// Change the role of the selected user in the team
const changeUserRoleInTeamMutation = useMutation(
ChangeUserRoleInTeamByAdminDocument
);
const changeUserRoleInTeam = (
userUID: string,
teamID: string,
newRole: TeamMemberRole
) => {
return changeUserRoleInTeamMutation.executeMutation({
userUID,
teamID,
newRole,
});
};
// Save the updates done to the team
const isLoading = ref(false);
const saveUpdatedTeam = async () => {
isLoading.value = true;
roleUpdates.value.forEach(async (update) => {
if (!team.value) return;
const updateMemberRoleResult = await changeUserRoleInTeam(
update.userID,
team.value.id,
update.role
);
if (updateMemberRoleResult.error) {
toast.error(`${t('state.role_update_failed')}`);
roleUpdates.value = [];
} else {
toast.success(`${t('state.role_update_success')}`);
roleUpdates.value = [];
}
isLoading.value = false;
});
};
// Remove user from the team mutation
const removeUserFromTeamMutation = useMutation(
RemoveUserFromTeamByAdminDocument
);
const removeUserFromTeam = (userUid: string, teamID: string) => {
return () =>
removeUserFromTeamMutation.executeMutation({
userUid,
teamID,
});
};
// Remove a team member from the team
const isLoadingIndex = ref<null | number>(null);
const removeExistingTeamMember = async (userID: string, index: number) => {
if (!team.value) return;
isLoadingIndex.value = index;
const removeTeamMemberResult = await removeUserFromTeam(
userID,
team.value.id
)();
if (removeTeamMemberResult.error) {
toast.error(`${t('state.remove_member_failure')}`);
} else {
team.value.teamMembers = team.value.teamMembers?.filter(
(member: any) => member.user.uid !== userID
);
toast.success(`${t('state.remove_member_success')}`);
}
isLoadingIndex.value = null;
emit('update-team');
};
</script>