Skip to content

Commit

Permalink
web/satellite: show project invitations in the Team page
Browse files Browse the repository at this point in the history
The Team page now displays project member invitations alongside project
members. These invitations can be removed in the same manner that
members can.

References #5855

Change-Id: Iade690757d4430deee3378066d7dc19766f53936
  • Loading branch information
jewharton authored and Storj Robot committed Jun 13, 2023
1 parent ed37d72 commit 8ee7d10
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 75 deletions.
14 changes: 11 additions & 3 deletions web/satellite/src/api/projectMembers.ts
Expand Up @@ -2,7 +2,7 @@
// See LICENSE for copying information.

import { BaseGql } from '@/api/baseGql';
import { ProjectMember, ProjectMemberCursor, ProjectMembersApi, ProjectMembersPage } from '@/types/projectMembers';
import { ProjectInvitationItemModel, ProjectMember, ProjectMemberCursor, ProjectMembersApi, ProjectMembersPage } from '@/types/projectMembers';
import { HttpClient } from '@/utils/httpClient';

export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
Expand Down Expand Up @@ -44,7 +44,7 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
project (
publicId: $projectId,
) {
members (
membersAndInvitations (
cursor: {
limit: $limit,
search: $search,
Expand All @@ -62,6 +62,10 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
},
joinedAt
},
projectInvitations {
email,
createdAt
},
search,
limit,
order,
Expand All @@ -83,7 +87,7 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {

const response = await this.query(query, variables);

return this.getProjectMembersList(response.data.project.members);
return this.getProjectMembersList(response.data.project.membersAndInvitations);
}

/**
Expand Down Expand Up @@ -120,6 +124,10 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
new Date(key.joinedAt),
key.user.id,
));
projectMembersPage.projectInvitations = projectMembers.projectInvitations.map(key => new ProjectInvitationItemModel(
key.email,
new Date(key.createdAt),
));

projectMembersPage.search = projectMembers.search;
projectMembersPage.limit = projectMembers.limit;
Expand Down
5 changes: 3 additions & 2 deletions web/satellite/src/components/common/TableItem.vue
Expand Up @@ -27,8 +27,7 @@
</p>
<p v-else :class="{primary: index === 0}" :title="val" @click.stop="(e) => cellContentClicked(index, e)">
<middle-truncate v-if="keyVal === 'fileName'" :text="val" />
<project-ownership-tag v-else-if="keyVal === 'owner'" :no-icon="itemType !== 'project'" :is-owner="val" />
<project-ownership-tag v-else-if="keyVal === 'invited'" :is-invited="val" />
<project-ownership-tag v-else-if="keyVal === 'role'" :no-icon="itemType !== 'project' && val !== ProjectRole.Invited" :role="val" />
<span v-else>{{ val }}</span>
</p>
<div v-if="showBucketGuide(index)" class="animation">
Expand All @@ -44,6 +43,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ProjectRole } from '@/types/projectMembers';
import VTableCheckbox from '@/components/common/VTableCheckbox.vue';
import BucketGuide from '@/components/objects/BucketGuide.vue';
import MiddleTruncate from '@/components/browser/MiddleTruncate.vue';
Expand Down
20 changes: 7 additions & 13 deletions web/satellite/src/components/project/ProjectOwnershipTag.vue
Expand Up @@ -2,37 +2,31 @@
// See LICENSE for copying information.

<template>
<div class="tag" :class="{owner: isOwner, invited: isInvited}">
<div class="tag" :class="{[role.toLowerCase()]: true}">
<component :is="icon" v-if="!noIcon" class="tag__icon" />

<span class="tag__text">{{ label }}</span>
<span class="tag__text">{{ role }}</span>
</div>
</template>

<script setup lang="ts">
import { computed, Component } from 'vue';
import { ProjectRole } from '@/types/projectMembers';
import BoxIcon from '@/../static/images/navigation/project.svg';
import InviteIcon from '@/../static/images/navigation/quickStart.svg';
const props = withDefaults(defineProps<{
isOwner: boolean,
isInvited: boolean,
role: ProjectRole,
noIcon?: boolean,
}>(), {
isOwner: false,
isInvited: false,
role: ProjectRole.Member,
noIcon: false,
});
const icon = computed((): string => {
return props.isInvited ? InviteIcon : BoxIcon;
});
const label = computed((): string => {
if (props.isOwner) return 'Owner';
if (props.isInvited) return 'Invited';
return 'Member';
return props.role === ProjectRole.Invited ? InviteIcon : BoxIcon;
});
</script>
Expand Down
Expand Up @@ -5,7 +5,7 @@
<div class="project-dashboard">
<div class="project-dashboard__heading">
<h1 class="project-dashboard__heading__title" aria-roledescription="title">{{ selectedProject.name }}</h1>
<project-ownership-tag :is-owner="selectedProject.ownerId === user.id" />
<project-ownership-tag :role="(selectedProject.ownerId === user.id) ? ProjectRole.Owner : ProjectRole.Member" />
</div>
<p class="project-dashboard__message">
Expect a delay of a few hours between network activity and the latest dashboard stats.
Expand Down Expand Up @@ -191,6 +191,7 @@ import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { centsToDollars } from '@/utils/strings';
import { User } from '@/types/users';
import { ProjectRole } from '@/types/projectMembers';
import VLoader from '@/components/common/VLoader.vue';
import InfoContainer from '@/components/project/dashboard/InfoContainer.vue';
Expand Down
35 changes: 25 additions & 10 deletions web/satellite/src/components/team/ProjectMemberListItem.vue
Expand Up @@ -7,16 +7,16 @@
:item="itemToRender"
:selectable="true"
:select-disabled="isProjectOwner"
:selected="itemData.isSelected"
:on-click="(_) => $emit('memberClick', itemData)"
:selected="model.isSelected()"
:on-click="(_) => $emit('memberClick', model)"
@selectClicked="($event) => $emit('selectClicked', $event)"
/>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { ProjectMember } from '@/types/projectMembers';
import { ProjectMember, ProjectMemberItemModel, ProjectRole } from '@/types/projectMembers';
import { useResize } from '@/composables/resize';
import { useProjectsStore } from '@/store/modules/projectsStore';
Expand All @@ -26,23 +26,38 @@ const { isMobile, isTablet } = useResize();
const projectsStore = useProjectsStore();
const props = withDefaults(defineProps<{
itemData: ProjectMember;
model: ProjectMemberItemModel;
}>(), {
itemData: () => new ProjectMember('', '', '', new Date(), ''),
model: () => new ProjectMember('', '', '', new Date(), ''),
});
const isProjectOwner = computed((): boolean => {
return props.itemData.user.id === projectsStore.state.selectedProject.ownerId;
return props.model.getUserID() === projectsStore.state.selectedProject.ownerId;
});
const itemToRender = computed((): { [key: string]: unknown | string[] } => {
if (!isMobile.value && !isTablet.value) return { name: props.itemData.name, email: props.itemData.email, owner: isProjectOwner.value, date: props.itemData.localDate() };
const itemToRender = computed((): { [key: string]: unknown } => {
let role: ProjectRole = ProjectRole.Member;
if (props.model.isPending()) {
role = ProjectRole.Invited;
} else if (isProjectOwner.value) {
role = ProjectRole.Owner;
}
if (!isMobile.value && !isTablet.value) {
const dateStr = props.model.getJoinDate().toLocaleDateString('en-US', { day:'numeric', month:'short', year:'numeric' });
return {
name: props.model.getName(),
email: props.model.getEmail(),
role: role,
date: dateStr,
};
}
if (isTablet.value) {
return { name: props.itemData.name, email: props.itemData.email, owner: isProjectOwner.value };
return { name: props.model.getName(), email: props.model.getEmail(), role: role };
}
// TODO: change after adding actions button to list item
return { name: props.itemData.name, email: props.itemData.email };
return { name: props.model.getName(), email: props.model.getEmail() };
});
</script>

Expand Down
16 changes: 8 additions & 8 deletions web/satellite/src/components/team/ProjectMembersArea.vue
Expand Up @@ -37,7 +37,7 @@
<ProjectMemberListItem
v-for="(member, key) in projectMembers"
:key="key"
:item-data="member"
:model="member"
@memberClick="onMemberCheckChange"
@selectClicked="(_) => onMemberCheckChange(member)"
/>
Expand All @@ -50,8 +50,8 @@
import { computed, onMounted, ref } from 'vue';
import {
ProjectMember,
ProjectMemberHeaderState,
ProjectMemberItemModel,
} from '@/types/projectMembers';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
Expand All @@ -77,10 +77,10 @@ const areMembersFetching = ref<boolean>(true);
* Returns team members of current page from store.
* With project owner pinned to top
*/
const projectMembers = computed((): ProjectMember[] => {
const projectMembers = pmStore.state.page.projectMembers;
const projectOwner = projectMembers.find((member) => member.user.id === projectsStore.state.selectedProject.ownerId);
const projectMembersToReturn = projectMembers.filter((member) => member.user.id !== projectsStore.state.selectedProject.ownerId);
const projectMembers = computed((): ProjectMemberItemModel[] => {
const projectMembers = pmStore.state.page.getAllItems();
const projectOwner = projectMembers.find((member) => member.getUserID() === projectsStore.state.selectedProject.ownerId);
const projectMembersToReturn = projectMembers.filter((member) => member.getUserID() !== projectsStore.state.selectedProject.ownerId);
// if the project owner exists, place at the front of the members list
projectOwner && projectMembersToReturn.unshift(projectOwner);
Expand Down Expand Up @@ -129,8 +129,8 @@ const isEmptySearchResultShown = computed((): boolean => {
* Selects team member if this user has no owner status.
* @param member
*/
function onMemberCheckChange(member: ProjectMember): void {
if (projectsStore.state.selectedProject.ownerId !== member.user.id) {
function onMemberCheckChange(member: ProjectMemberItemModel): void {
if (projectsStore.state.selectedProject.ownerId !== member.getUserID()) {
pmStore.toggleProjectMemberSelection(member);
}
}
Expand Down
29 changes: 12 additions & 17 deletions web/satellite/src/store/modules/projectMembersStore.ts
Expand Up @@ -7,6 +7,7 @@ import { defineStore } from 'pinia';
import {
ProjectMember,
ProjectMemberCursor,
ProjectMemberItemModel,
ProjectMemberOrderBy,
ProjectMembersApi,
ProjectMembersPage,
Expand Down Expand Up @@ -43,12 +44,8 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
const projectMembersPage: ProjectMembersPage = await api.get(projectID, state.cursor);

state.page = projectMembersPage;
state.page.projectMembers = state.page.projectMembers.map(member => {
if (state.selectedProjectMembersEmails.includes(member.user.email)) {
member.isSelected = true;
}

return member;
state.page.getAllItems().forEach(item => {
item.setSelected(state.selectedProjectMembersEmails.includes(item.getEmail()));
});

return projectMembersPage;
Expand Down Expand Up @@ -78,27 +75,25 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
state.cursor.orderDirection = direction;
}

function toggleProjectMemberSelection(projectMember: ProjectMember) {
if (!state.selectedProjectMembersEmails.includes(projectMember.user.email)) {
projectMember.isSelected = true;
state.selectedProjectMembersEmails.push(projectMember.user.email);
function toggleProjectMemberSelection(projectMember: ProjectMemberItemModel) {
const email = projectMember.getEmail();

if (!state.selectedProjectMembersEmails.includes(email)) {
projectMember.setSelected(true);
state.selectedProjectMembersEmails.push(email);

return;
}

projectMember.isSelected = false;
projectMember.setSelected(false);
state.selectedProjectMembersEmails = state.selectedProjectMembersEmails.filter(projectMemberEmail => {
return projectMemberEmail !== projectMember.user.email;
return projectMemberEmail !== email;
});
}

function clearProjectMemberSelection() {
state.selectedProjectMembersEmails = [];
state.page.projectMembers = state.page.projectMembers.map((projectMember: ProjectMember) => {
projectMember.isSelected = false;

return projectMember;
});
state.page.getAllItems().forEach(member => member.setSelected(false));
}

function clear() {
Expand Down

0 comments on commit 8ee7d10

Please sign in to comment.