web/satellite: add invites to all projects dashboard table

This change adds projects invites to the projects table.

Related: https://github.com/storj/storj/issues/5925

Change-Id: I82fbf1e47ea383377369005956a9eec3c8ac6416
This commit is contained in:
Wilfred Asomani 2023-06-06 18:30:29 +00:00
parent 771ec3237a
commit 8cdc5bd107
6 changed files with 235 additions and 4 deletions

View File

@ -28,6 +28,7 @@
<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" />
<span v-else>{{ val }}</span>
</p>
<div v-if="showBucketGuide(index)" class="animation">

View File

@ -248,6 +248,14 @@ export class ProjectInvitation {
public inviterEmail: string,
public createdAt: Date,
) {}
/**
* Returns created date as a local string.
*/
public get invitedDate(): string {
const createdAt = new Date(this.createdAt);
return createdAt.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: 'numeric' });
}
}
/**

View File

@ -32,7 +32,7 @@
</span>
</div>
<div v-if="projects.length || invites.length" class="my-projects__list">
<projects-table v-if="isTableViewSelected" class="my-projects__list__table" />
<projects-table v-if="isTableViewSelected" :invites="invites" class="my-projects__list__table" />
<div v-else-if="!isTableViewSelected" class="my-projects__list__cards">
<project-item v-for="project in projects" :key="project.id" :project="project" />
<project-invitation-item v-for="invite in invites" :key="invite.projectID" :invitation="invite" />

View File

@ -0,0 +1,209 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<table-item
item-type="project"
:item="itemToRender"
class="invitation-item"
>
<template #options>
<th class="options overflow-visible">
<v-button
:loading="isLoading"
:disabled="isLoading"
:on-press="onJoinClicked"
border-radius="8px"
font-size="12px"
label="Join Project"
class="invitation-item__menu__button"
/>
<div class="invitation-item__menu">
<div class="invitation-item__menu__icon" @click.stop="toggleDropDown">
<div class="invitation-item__menu__icon__content" :class="{open: isDropdownOpen}">
<menu-icon />
</div>
</div>
<div v-if="isDropdownOpen" v-click-outside="closeDropDown" class="invitation-item__menu__dropdown">
<div class="invitation-item__menu__dropdown__item" @click.stop="onDeclineClicked">
<logout-icon />
<p class="invitation-item__menu__dropdown__item__label">Decline invite</p>
</div>
</div>
</div>
</th>
</template>
<menu-icon />
</table-item>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { ProjectInvitation, ProjectInvitationResponse } from '@/types/projects';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useResize } from '@/composables/resize';
import { AnalyticsHttpApi } from '@/api/analytics';
import VButton from '@/components/common/VButton.vue';
import TableItem from '@/components/common/TableItem.vue';
import MenuIcon from '@/../static/images/common/horizontalDots.svg';
import LogoutIcon from '@/../static/images/navigation/logout.svg';
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const isLoading = ref<boolean>(false);
const props = defineProps<{
invitation: ProjectInvitation,
}>();
const { isMobile } = useResize();
const itemToRender = computed((): { [key: string]: unknown | string[] } => {
if (!isMobile.value) {
return {
multi: { title: props.invitation.projectName, subtitle: props.invitation.projectDescription },
date: props.invitation.invitedDate,
memberCount: '',
invited: true,
};
}
return { info: [ props.invitation.projectName, `Created ${props.invitation.invitedDate}` ] };
});
/**
* isDropdownOpen if dropdown is open.
*/
const isDropdownOpen = computed((): boolean => {
return appStore.state.activeDropdown === props.invitation.projectID;
});
/**
* Displays the Join Project modal.
*/
function onJoinClicked(): void {
projectsStore.selectInvitation(props.invitation);
appStore.updateActiveModal(MODALS.joinProject);
}
/**
* Declines the project member invitation.
*/
async function onDeclineClicked(): Promise<void> {
if (isLoading.value) return;
isLoading.value = true;
try {
await projectsStore.respondToInvitation(props.invitation.projectID, ProjectInvitationResponse.Decline);
analytics.eventTriggered(AnalyticsEvent.PROJECT_INVITATION_DECLINED);
} catch (error) {
notify.error(`Failed to decline project invitation. ${error.message}`, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
notify.error(`Failed to reload projects and invitations list. ${error.message}`, AnalyticsErrorEventSource.PROJECT_INVITATION);
}
isLoading.value = false;
}
function toggleDropDown() {
appStore.toggleActiveDropdown(props.invitation.projectID);
}
function closeDropDown() {
appStore.closeDropdowns();
}
</script>
<style scoped lang="scss">
.invitation-item {
.options {
display: flex;
align-items: center;
justify-content: flex-end;
column-gap: 10px;
padding-right: 10px;
}
&__menu {
position: relative;
cursor: pointer;
&__button {
padding: 10px 16px;
}
&__icon {
&__content {
height: 32px;
width: 32px;
padding: 12px 5px;
border-radius: 5px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
&.open {
background: var(--c-grey-3);
}
}
}
&__dropdown {
position: absolute;
top: 40px;
right: 0;
background: #fff;
box-shadow: 0 7px 20px rgb(0 0 0 / 15%);
border: 1px solid var(--c-grey-2);
border-radius: 8px;
z-index: 100;
overflow: hidden;
&__item {
display: flex;
align-items: center;
width: 200px;
padding: 15px;
color: var(--c-grey-6);
cursor: pointer;
&__label {
font-family: 'font_regular', sans-serif;
margin: 0 0 0 10px;
}
&:hover {
font-family: 'font_medium', sans-serif;
color: var(--c-blue-3);
background-color: var(--c-grey-1);
svg :deep(path) {
fill: var(--c-blue-3);
}
}
}
}
}
}
</style>

View File

@ -206,7 +206,8 @@ async function goToProjectEdit(): Promise<void> {
&__item {
display: flex;
align-items: center;
padding: 15px 25px;
width: 200px;
padding: 15px;
color: var(--c-grey-6);
cursor: pointer;

View File

@ -3,7 +3,7 @@
<template>
<v-table
:total-items-count="projects.length"
:total-items-count="projects.length + invites?.length || 0"
class="projects-table"
items-label="projects"
>
@ -14,6 +14,11 @@
<th class="sort-header-container__date-item align-left">Role</th>
</template>
<template #body>
<project-table-invitation-item
v-for="(invite, key) in invites"
:key="key"
:invitation="invite"
/>
<project-table-item
v-for="(project, key) in projects"
:key="key"
@ -26,14 +31,21 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Project } from '@/types/projects';
import { Project, ProjectInvitation } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
import ProjectTableItem from '@/views/all-dashboard/components/ProjectTableItem.vue';
import ProjectTableInvitationItem from '@/views/all-dashboard/components/ProjectTableInvitationItem.vue';
import VTable from '@/components/common/VTable.vue';
const projectsStore = useProjectsStore();
const props = withDefaults(defineProps<{
invites?: ProjectInvitation[],
}>(), {
invites: () => [],
});
/**
* Returns projects list from store.
*/