web/satellite: show project invitations in dashboard area
This change allows users to view and interact with their project member invitations through a banner in the dashboard area. The banner only appears if the All Projects Dashboard is disabled, as it provides the same functionality in a different way. References #5855 Change-Id: Ia0771e2af52c40a72f1cacf72bc9098cc68f0dcd
This commit is contained in:
parent
cafa6db971
commit
c858479ef0
@ -0,0 +1,171 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div v-if="invite" class="banner">
|
||||
<VLoader v-if="isLoading" class="banner__loader" width="40px" height="40px" />
|
||||
<span v-if="invites.length > 1" class="banner__count">{{ invites.length }}</span>
|
||||
<div class="banner__left">
|
||||
<UsersIcon class="banner__left__icon" />
|
||||
<span>{{ invite.inviterEmail }} has invited you to the project "{{ invite.projectName }}".</span>
|
||||
</div>
|
||||
<div class="banner__right">
|
||||
<div class="banner__right__links">
|
||||
<a @click="onJoinClicked">Join Project</a>
|
||||
<a @click="onDeclineClicked">Decline</a>
|
||||
</div>
|
||||
<CloseIcon class="banner__right__close" @click="onCloseClicked" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ProjectInvitation, ProjectInvitationResponse } from '@/types/projects';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
|
||||
import VLoader from '@/components/common/VLoader.vue';
|
||||
|
||||
import UsersIcon from '@/../static/images/notifications/usersIcon.svg';
|
||||
import CloseIcon from '@/../static/images/notifications/closeSmall.svg';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const notify = useNotify();
|
||||
|
||||
const isLoading = ref<boolean>(false);
|
||||
const hidden = ref<Set<ProjectInvitation>>(new Set<ProjectInvitation>());
|
||||
|
||||
/**
|
||||
* Returns a sorted list of non-hidden project member invitation from the store.
|
||||
*/
|
||||
const invites = computed((): ProjectInvitation[] => {
|
||||
return projectsStore.state.invitations
|
||||
.filter(invite => !hidden.value.has(invite))
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the first non-hidden project member invitation from the store.
|
||||
*/
|
||||
const invite = computed((): ProjectInvitation | null => {
|
||||
return !invites.value.length ? null : invites.value[0];
|
||||
});
|
||||
|
||||
/**
|
||||
* Hides the active project member invitation.
|
||||
* Closes the notification if there are no more invitations.
|
||||
*/
|
||||
function onCloseClicked(): void {
|
||||
if (isLoading.value || !invite.value) return;
|
||||
hidden.value.add(invite.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the Join Project modal.
|
||||
*/
|
||||
function onJoinClicked(): void {
|
||||
if (isLoading.value || !invite.value) return;
|
||||
projectsStore.selectInvitation(invite.value);
|
||||
appStore.updateActiveModal(MODALS.joinProject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declines the project member invitation.
|
||||
*/
|
||||
async function onDeclineClicked(): Promise<void> {
|
||||
if (isLoading.value || !invite.value) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
await projectsStore.respondToInvitation(invite.value.projectID, ProjectInvitationResponse.Decline);
|
||||
} catch (error) {
|
||||
notify.error(`Failed to decline project invitation. ${error.message}`, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
await projectsStore.getUserInvitations();
|
||||
await projectsStore.getProjects();
|
||||
} catch (error) {
|
||||
notify.error(`Failed to reload projects and invitations list. ${error.message}`, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
|
||||
}
|
||||
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.banner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: var(--c-white);
|
||||
border: 1px solid var(--c-grey-3);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 7px 20px rgb(0 0 0 / 15%);
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
&__count {
|
||||
padding: 2px 8px;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--c-blue-3);
|
||||
color: var(--c-white);
|
||||
}
|
||||
|
||||
&__loader {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(255 255 255 / 66%);
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 23px;
|
||||
text-align: center;
|
||||
|
||||
& a {
|
||||
color: var(--c-black);
|
||||
line-height: 22px;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -35,7 +35,6 @@ const props = defineProps<{
|
||||
background-color: #fff;
|
||||
border: 1px solid rgb(56 75 101 / 40%);
|
||||
padding: 16px;
|
||||
margin-bottom: 48px;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 14px;
|
||||
border-radius: 16px;
|
||||
|
@ -15,7 +15,9 @@
|
||||
<div ref="dashboardContent" class="dashboard__wrap__main-area__content-wrap__container">
|
||||
<BetaSatBar v-if="isBetaSatellite" />
|
||||
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
|
||||
<div class="banner-container dashboard__wrap__main-area__content-wrap__container__content">
|
||||
<div class="dashboard__wrap__main-area__content-wrap__container__content banners">
|
||||
<ProjectInvitationBanner v-if="isProjectInvitationBannerShown" />
|
||||
|
||||
<UpdateSessionTimeoutBanner
|
||||
v-if="isUpdateSessionTimeoutBanner && dashboardContent"
|
||||
:dashboard-ref="dashboardContent"
|
||||
@ -78,7 +80,7 @@
|
||||
</v-banner>
|
||||
</div>
|
||||
<router-view class="dashboard__wrap__main-area__content-wrap__container__content" />
|
||||
<div class="banner-container__bottom dashboard__wrap__main-area__content-wrap__container__content">
|
||||
<div class="dashboard__wrap__main-area__content-wrap__container__content banners-bottom">
|
||||
<UploadNotification
|
||||
v-if="isLargeUploadNotificationShown && !isLargeUploadWarningNotificationShown && isBucketsView"
|
||||
wording-bold="The web browser is best for uploads up to 1GB."
|
||||
@ -173,6 +175,7 @@ import LimitWarningModal from '@/components/modals/LimitWarningModal.vue';
|
||||
import VBanner from '@/components/common/VBanner.vue';
|
||||
import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue';
|
||||
import ProjectLimitBanner from '@/components/notifications/ProjectLimitBanner.vue';
|
||||
import ProjectInvitationBanner from '@/components/notifications/ProjectInvitationBanner.vue';
|
||||
import BrandedLoader from '@/components/common/BrandedLoader.vue';
|
||||
import UpdateSessionTimeoutBanner from '@/components/notifications/UpdateSessionTimeoutBanner.vue';
|
||||
import ObjectsUploadingModal from '@/components/modals/objectUpload/ObjectsUploadingModal.vue';
|
||||
@ -434,6 +437,13 @@ const isLargeUploadWarningNotificationShown = computed((): boolean => {
|
||||
return appStore.state.isLargeUploadWarningNotificationShown;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates whether the project member invitation banner should be shown.
|
||||
*/
|
||||
const isProjectInvitationBannerShown = computed((): boolean => {
|
||||
return !configStore.state.config.allProjectsDashboard;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates if current route is create project page.
|
||||
*/
|
||||
@ -783,6 +793,12 @@ onMounted(async () => {
|
||||
notify.error(`Unable to get credit cards. ${error.message}`, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
await projectsStore.getUserInvitations();
|
||||
} catch (error) {
|
||||
notify.error(`Unable to get project invitations. ${error.message}`, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
|
||||
}
|
||||
|
||||
let projects: Project[] = [];
|
||||
|
||||
try {
|
||||
@ -824,25 +840,6 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.notification-wrap) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.banner-container {
|
||||
padding-top: 0 !important;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
height: 100%;
|
||||
background-color: #f5f6fa;
|
||||
@ -881,6 +878,30 @@ onBeforeUnmount(() => {
|
||||
padding: 48px 48px 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
&.banners {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.banners-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 48px;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
|
||||
&:empty {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -926,11 +947,7 @@ onBeforeUnmount(() => {
|
||||
@media screen and (width <= 800px) {
|
||||
|
||||
.dashboard__wrap__main-area__content-wrap__container__content {
|
||||
padding: 32px 24px 50px;
|
||||
}
|
||||
|
||||
.banner-container {
|
||||
padding-bottom: 0;
|
||||
padding: 32px 24px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
6
web/satellite/static/images/notifications/usersIcon.svg
Normal file
6
web/satellite/static/images/notifications/usersIcon.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.1231 25.2746C17.9508 26.9128 17.2608 28.9197 17.2608 31.0878V34.0008H2V32.24C2 26.7291 6.46741 22.2617 11.9782 22.2617C14.7793 22.2617 17.3108 23.4159 19.1231 25.2746Z" fill="#376FFF"/>
|
||||
<circle cx="12.2244" cy="14.1571" r="7.15706" fill="#00E075"/>
|
||||
<path d="M18.4346 32.24C18.4346 26.7291 22.902 22.2617 28.4128 22.2617C33.9236 22.2617 38.391 26.7291 38.391 32.24V34.0008H18.4346V32.24Z" fill="#FF598B"/>
|
||||
<circle cx="28.5828" cy="14.1571" r="7.15706" fill="#FFC700"/>
|
||||
</svg>
|
After Width: | Height: | Size: 628 B |
Loading…
Reference in New Issue
Block a user