@@ -44,6 +43,8 @@
diff --git a/web/satellite/src/components/project/dashboard/ProjectDashboard.vue b/web/satellite/src/components/project/dashboard/ProjectDashboard.vue
index 11df15f97..340599c87 100644
--- a/web/satellite/src/components/project/dashboard/ProjectDashboard.vue
+++ b/web/satellite/src/components/project/dashboard/ProjectDashboard.vue
@@ -5,7 +5,7 @@
{{ selectedProject.name }}
-
+
Expect a delay of a few hours between network activity and the latest dashboard stats.
@@ -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';
diff --git a/web/satellite/src/components/team/ProjectMemberListItem.vue b/web/satellite/src/components/team/ProjectMemberListItem.vue
index c6fc19ac1..2e371f51e 100644
--- a/web/satellite/src/components/team/ProjectMemberListItem.vue
+++ b/web/satellite/src/components/team/ProjectMemberListItem.vue
@@ -7,8 +7,8 @@
: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)"
/>
@@ -16,7 +16,7 @@
diff --git a/web/satellite/src/components/team/ProjectMembersArea.vue b/web/satellite/src/components/team/ProjectMembersArea.vue
index 6ee10ab42..b23be2e7a 100644
--- a/web/satellite/src/components/team/ProjectMembersArea.vue
+++ b/web/satellite/src/components/team/ProjectMembersArea.vue
@@ -37,7 +37,7 @@
onMemberCheckChange(member)"
/>
@@ -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';
@@ -77,10 +77,10 @@ const areMembersFetching = ref(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);
@@ -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);
}
}
diff --git a/web/satellite/src/store/modules/projectMembersStore.ts b/web/satellite/src/store/modules/projectMembersStore.ts
index 7ccca4c98..458e1f84a 100644
--- a/web/satellite/src/store/modules/projectMembersStore.ts
+++ b/web/satellite/src/store/modules/projectMembersStore.ts
@@ -7,6 +7,7 @@ import { defineStore } from 'pinia';
import {
ProjectMember,
ProjectMemberCursor,
+ ProjectMemberItemModel,
ProjectMemberOrderBy,
ProjectMembersApi,
ProjectMembersPage,
@@ -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;
@@ -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() {
diff --git a/web/satellite/src/types/projectMembers.ts b/web/satellite/src/types/projectMembers.ts
index b72d054e3..ed5228d16 100644
--- a/web/satellite/src/types/projectMembers.ts
+++ b/web/satellite/src/types/projectMembers.ts
@@ -84,6 +84,7 @@ export class ProjectMemberCursor {
export class ProjectMembersPage {
public constructor(
public projectMembers: ProjectMember[] = [],
+ public projectInvitations: ProjectInvitationItemModel[] = [],
public search: string = '',
public order: ProjectMemberOrderBy = ProjectMemberOrderBy.NAME,
public orderDirection: SortDirection = SortDirection.ASCENDING,
@@ -92,14 +93,73 @@ export class ProjectMembersPage {
public currentPage: number = 1,
public totalCount: number = 0,
) {}
+
+ /**
+ * Returns all project members and invitations as ProjectMemberItemModel.
+ */
+ public getAllItems(): ProjectMemberItemModel[] {
+ const items = (this.projectMembers as ProjectMemberItemModel[]).concat(this.projectInvitations);
+ return items.sort((a, b) => {
+ let cmp: (a: ProjectMemberItemModel, b: ProjectMemberItemModel) => number;
+
+ if (this.order === ProjectMemberOrderBy.CREATED_AT) {
+ cmp = (a, b) => a.getJoinDate().getTime() - b.getJoinDate().getTime();
+ } else {
+ cmp = (a, b) => a.getName().toLowerCase().localeCompare(b.getName().toLowerCase());
+ }
+
+ const result = (this.orderDirection === SortDirection.DESCENDING) ? cmp(b, a) : cmp(a, b);
+ return (result !== 0) ? result : a.getEmail().toLowerCase().localeCompare(b.getEmail().toLowerCase());
+ });
+ }
+}
+
+/**
+ * ProjectInvitationItemModel represents the view model for project member list items.
+ */
+export interface ProjectMemberItemModel {
+ /**
+ * Returns the member's user ID if it exists.
+ */
+ getUserID(): string | null;
+
+ /**
+ * Returns the member's name.
+ */
+ getName(): string;
+
+ /**
+ * Returns the member's email address.
+ */
+ getEmail(): string,
+
+ /**
+ * Returns the date that the member joined the project.
+ */
+ getJoinDate(): Date;
+
+ /**
+ * Sets whether the member item has been selected.
+ */
+ setSelected(selected: boolean): void;
+
+ /**
+ * Returns whether the member item has been selected.
+ */
+ isSelected(): boolean;
+
+ /**
+ * Returns whether the member has yet to accept its invitation.
+ */
+ isPending(): boolean;
}
/**
* ProjectMember is a type, used to describe project member.
*/
-export class ProjectMember {
+export class ProjectMember implements ProjectMemberItemModel {
public user: User;
- public isSelected: boolean;
+ public _isSelected = false;
public constructor(
public fullName: string = '',
@@ -109,20 +169,126 @@ export class ProjectMember {
public id: string = '',
) {
this.user = new User(this.id, this.fullName, this.shortName, this.email);
- this.isSelected = false;
+ }
+
+ /**
+ * Returns the user's ID.
+ */
+ public getUserID(): string | null {
+ return this.id;
}
/**
* Returns user's full name.
*/
- public get name(): string {
+ public getName(): string {
return this.user.getFullName();
}
/**
- * Returns joined at date as a local date string.
+ * Returns user's email address.
*/
- public localDate(): string {
- return this.joinedAt.toLocaleDateString('en-US', { day:'numeric', month:'short', year:'numeric' });
+ public getEmail(): string {
+ return this.email;
+ }
+
+ /**
+ * Returns the date that the member joined the project.
+ */
+ public getJoinDate(): Date {
+ return this.joinedAt;
+ }
+
+ /**
+ * Sets whether the member item has been selected.
+ */
+ public setSelected(selected: boolean): void {
+ this._isSelected = selected;
+ }
+
+ /**
+ * Returns whether the member item has been selected.
+ */
+ public isSelected(): boolean {
+ return this._isSelected;
+ }
+
+ /**
+ * Returns whether the member has yet to accept its invitation.
+ * Always false. Required for implementing ProjectMemberItemModel.
+ */
+ public isPending(): boolean {
+ return false;
}
}
+
+/**
+ * ProjectInvitationItemModel represents the view model for project member invitation list items.
+ */
+export class ProjectInvitationItemModel implements ProjectMemberItemModel {
+ private _isSelected = false;
+
+ public constructor(
+ public email: string,
+ public createdAt: Date,
+ ) {}
+
+ /**
+ * Returns a null user ID. Required for implementing ProjectMemberItemModel.
+ */
+ public getUserID(): string | null {
+ return null;
+ }
+
+ /**
+ * Returns the placeholder name of the invitation recipient.
+ */
+ public getName(): string {
+ return this.getEmail().split('@')[0];
+ }
+
+ /**
+ * Returns the invitation recipient's email address.
+ */
+ public getEmail(): string {
+ return this.email.toLowerCase();
+ }
+
+ /**
+ * Returns the date that the invitation was created.
+ */
+ public getJoinDate(): Date {
+ return this.createdAt;
+ }
+
+ /**
+ * Sets whether the invitation item has been selected.
+ */
+ public setSelected(selected: boolean): void {
+ this._isSelected = selected;
+ }
+
+ /**
+ * Returns whether the invitation item has been selected.
+ */
+ public isSelected(): boolean {
+ return this._isSelected;
+ }
+
+ /**
+ * Returns whether the member has yet to accept its invitation.
+ * Always true. Required for implementing ProjectMemberItemModel.
+ */
+ public isPending(): boolean {
+ return true;
+ }
+}
+
+/**
+ * ProjectRole represents a project member's role.
+ */
+export enum ProjectRole {
+ Member = 'Member',
+ Owner = 'Owner',
+ Invited = 'Invited',
+}
diff --git a/web/satellite/src/views/all-dashboard/components/ProjectInvitationItem.vue b/web/satellite/src/views/all-dashboard/components/ProjectInvitationItem.vue
index 7089a6f42..83d001b70 100644
--- a/web/satellite/src/views/all-dashboard/components/ProjectInvitationItem.vue
+++ b/web/satellite/src/views/all-dashboard/components/ProjectInvitationItem.vue
@@ -3,7 +3,7 @@
-
+
{{ invitation.projectName }}
@@ -38,6 +38,7 @@