web/satellite: remove project member modal

Create modal for removing project members

issue: https://github.com/storj/storj/issues/5751

Change-Id: I621b8e1de54270faebb7577b8431903c62a6e45c
This commit is contained in:
Cameron 2023-04-27 16:53:15 -04:00 committed by Storj Robot
parent fb1a0cc784
commit e8e6dd056a
6 changed files with 285 additions and 77 deletions

View File

@ -181,16 +181,16 @@ function handleClick(): void {
}
.solid-red {
background-color: var(--c-red-3) !important;
border: 1px solid var(--c-red-3) !important;
background-color: #ff1313 !important;
border: 1px solid #ff1313 !important;
.label {
color: #fff !important;
}
&:hover {
background-color: #790000 !important;
border: 1px solid #790000 !important;
background-color: #c90e0e !important;
border: 1px solid #c90e0e !important;
}
}

View File

@ -0,0 +1,245 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<div class="modal__header">
<TeamMemberIcon />
<h1 class="modal__header__title">Remove Member</h1>
</div>
<p class="modal__info">
The following team members will be removed. This action cannot be undone.
</p>
<div class="modal__pm-container">
<div v-for="(member, key) in firstThreeSelected" :key="key" class="modal__project-member">
{{ member }}
</div>
<div v-if="selectedMembersLength > 3" class="modal__project-member">
+ {{ selectedMembersLength - 3 }} more
</div>
<div class="modal__notice">
<strong>Please note:</strong> any access grants they have created will still provide them with full access. If necessary, please revoke these access grants to ensure the security of your data.
</div>
</div>
<div class="modal__button-container">
<VButton
label="Cancel"
width="100%"
height="48px"
font-size="14px"
border-radius="10px"
:on-press="closeModal"
:is-transparent="true"
/>
<VButton
label="Remove"
:is-solid-delete="true"
icon="trash"
width="100%"
height="48px"
font-size="14px"
border-radius="10px"
:on-press="onRemove"
/>
</div>
</div>
</template>
</VModal>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { RouteConfig } from '@/router';
import { AnalyticsHttpApi } from '@/api/analytics';
import { Project } from '@/types/projects';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useNotify, useRouter } from '@/utils/hooks';
import { LocalData } from '@/utils/localData';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useConfigStore } from '@/store/modules/configStore';
import VLoader from '@/components/common/VLoader.vue';
import VInput from '@/components/common/VInput.vue';
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import TeamMemberIcon from '@/../static/images/team/teamMember.svg';
const FIRST_PAGE = 1;
const configStore = useConfigStore();
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const pmStore = useProjectMembersStore();
const notify = useNotify();
const router = useRouter();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const firstThreeSelected = computed((): string[] => {
return pmStore.state.selectedProjectMembersEmails.slice(0, 3);
});
const selectedMembersLength = computed((): number => {
return pmStore.state.selectedProjectMembersEmails.length;
});
async function setProjectState(): Promise<void> {
const projects: Project[] = await projectsStore.getProjects();
if (!projects.length) {
const onboardingPath = RouteConfig.OnboardingTour.with(configStore.firstOnboardingStep).path;
analytics.pageVisit(onboardingPath);
router.push(onboardingPath);
return;
}
if (!projects.includes(projectsStore.state.selectedProject)) {
projectsStore.selectProject(projects[0].id);
}
await pmStore.getProjectMembers(FIRST_PAGE, projectsStore.state.selectedProject.id);
}
async function onRemove(): Promise<void> {
try {
await pmStore.deleteProjectMembers(projectsStore.state.selectedProject.id);
await setProjectState();
} catch (error) {
await notify.error(`Error while deleting users from projectMembers. ${error.message}`, AnalyticsErrorEventSource.PROJECT_MEMBERS_HEADER);
return;
}
await notify.success('Members were successfully removed from project');
pmStore.setSearchQuery('');
closeModal();
}
/**
* Closes remove team member modal.
*/
function closeModal(): void {
appStore.updateActiveModal(MODALS.removeTeamMember);
}
</script>
<style scoped lang="scss">
.modal {
padding: 32px;
font-family: 'font_regular', sans-serif;
display: flex;
flex-direction: column;
max-width: 350px;
@media screen and (max-width: 615px) {
padding: 30px 20px;
}
&__blur {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgb(229 229 229 / 20%);
border-radius: 8px;
z-index: 100;
&__loader {
width: 25px;
height: 25px;
position: absolute;
right: 40px;
top: 40px;
}
}
:deep(.label-container) {
margin-bottom: 8px;
}
:deep(.label-container__main__label) {
font-family: 'font_bold', sans-serif;
font-size: 14px;
color: #56606d;
}
&__header {
display: flex;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--c-grey-2);
&__title {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 31px;
color: var(--c-grey-8);
margin-left: 16px;
text-align: left;
}
}
&__info {
font-size: 14px;
line-height: 20px;
color: var(--c-blue-6);
padding: 16px 0;
border-bottom: 1px solid var(--c-grey-2);
text-align: left;
}
&__pm-container {
border-bottom: 1px solid var(--c-grey-2);
}
&__button-container {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
column-gap: 20px;
@media screen and (max-width: 600px) {
margin-top: 20px;
column-gap: unset;
row-gap: 8px;
flex-direction: column-reverse;
}
}
&__notice {
background: #fec;
border: 1px solid #ffd78a;
box-shadow: 0 7px 20px rgb(0 0 0 / 15%);
border-radius: 10px;
padding: 16px;
gap: 16px;
text-align: left;
margin: 16px 0;
strong {
font-family: 'font_bold', sans-serif;
}
}
&__project-member {
width: fit-content;
text-align: left;
background: #f4f5f7;
border-radius: 30px;
padding: 7px 20px;
gap: 10px;
margin: 16px 0;
font-family: 'font_medium', sans-serif;
}
}
</style>

View File

@ -27,7 +27,7 @@
height="40px"
font-size="13px"
border-radius="8px"
:on-press="toggleTeamMembersModal"
:on-press="toggleAddTeamMembersModal"
icon="add"
:is-disabled="isAddButtonDisabled"
/>
@ -49,7 +49,7 @@
label="Delete"
width="122px"
height="40px"
:on-press="onFirstDeleteClick"
:on-press="toggleRemoveTeamMembersModal"
/>
<VButton
class="button"
@ -60,35 +60,13 @@
:on-press="onClearSelection"
/>
</div>
<div v-if="areSelectedProjectMembersBeingDeleted" class="header-after-delete-click">
<span class="header-after-delete-click__delete-confirmation">Are you sure you want to delete <b>{{ selectedProjectMembersCount }}</b> {{ userCountTitle }}?</span>
<div class="header-after-delete-click__button-area">
<VButton
class="button deletion"
label="Delete"
width="122px"
height="40px"
:on-press="onDelete"
/>
<VButton
class="button"
label="Cancel"
width="122px"
height="40px"
:is-transparent="true"
:on-press="onClearSelection"
/>
</div>
</div>
</div>
<div v-if="isDeleteClicked" class="blur-content" />
<div v-if="isDeleteClicked" class="blur-search" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue';
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { RouteConfig } from '@/router';
import { ProjectMemberHeaderState } from '@/types/projectMembers';
@ -129,8 +107,6 @@ const props = withDefaults(defineProps<{
isAddButtonDisabled: false,
});
const emit = defineEmits(['onSuccessAction']);
const FIRST_PAGE = 1;
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
@ -152,10 +128,6 @@ const areProjectMembersSelected = computed((): boolean => {
return props.headerState === 1 && !isDeleteClicked.value;
});
const areSelectedProjectMembersBeingDeleted = computed((): boolean => {
return props.headerState === 1 && isDeleteClicked.value;
});
const userCountTitle = computed((): string => {
return props.selectedProjectMembersCount === 1 ? 'user' : 'users';
});
@ -163,12 +135,12 @@ const userCountTitle = computed((): string => {
/**
* Opens add team members modal.
*/
function toggleTeamMembersModal(): void {
function toggleAddTeamMembersModal(): void {
appStore.updateActiveModal(MODALS.addTeamMember);
}
function onFirstDeleteClick(): void {
isDeleteClicked.value = true;
function toggleRemoveTeamMembersModal(): void {
appStore.updateActiveModal(MODALS.removeTeamMember);
}
/**
@ -177,26 +149,6 @@ function onFirstDeleteClick(): void {
function onClearSelection(): void {
pmStore.clearProjectMemberSelection();
isDeleteClicked.value = false;
emit('onSuccessAction');
}
/**
* Removes user from selected project.
*/
async function onDelete(): Promise<void> {
try {
await pmStore.deleteProjectMembers(projectsStore.state.selectedProject.id);
await setProjectState();
} catch (error) {
await notify.error(`Error while deleting users from projectMembers. ${error.message}`, AnalyticsErrorEventSource.PROJECT_MEMBERS_HEADER);
isDeleteClicked.value = false;
return;
}
emit('onSuccessAction');
await notify.success('Members were successfully removed from project');
isDeleteClicked.value = false;
}
/**
@ -204,7 +156,10 @@ async function onDelete(): Promise<void> {
* @param search
*/
async function processSearchQuery(search: string): Promise<void> {
pmStore.setSearchQuery(search);
// avoid infinite loop due to listener on pmStore.setSearchQuery('') itself indirectly calling pmStore.setSearchQuery('')
if (pmStore.getSearchQuery() !== search) {
pmStore.setSearchQuery(search);
}
try {
await pmStore.getProjectMembers(FIRST_PAGE, projectsStore.state.selectedProject.id);
} catch (error) {
@ -212,24 +167,17 @@ async function processSearchQuery(search: string): Promise<void> {
}
}
async function setProjectState(): Promise<void> {
const projects: Project[] = await projectsStore.getProjects();
if (!projects.length) {
const onboardingPath = RouteConfig.OnboardingTour.with(configStore.firstOnboardingStep).path;
analytics.pageVisit(onboardingPath);
router.push(onboardingPath);
return;
}
if (!projects.includes(projectsStore.state.selectedProject)) {
projectsStore.selectProject(projects[0].id);
}
await pmStore.getProjectMembers(FIRST_PAGE, projectsStore.state.selectedProject.id);
searchInput.value?.clearSearch();
}
/**
* Lifecycle hook after initial render.
* Set up listener to clear search bar.
*/
onMounted((): void => {
pmStore.$onAction(({ name, after, args }) => {
if (name === 'setSearchQuery' && args[0] === '') {
after((_) => searchInput.value?.clearSearch());
}
});
});
/**
* Lifecycle hook before component destruction.

View File

@ -66,6 +66,10 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
state.cursor.search = search;
}
function getSearchQuery() {
return state.cursor.search;
}
function setSortingBy(order: ProjectMemberOrderBy) {
state.cursor.order = order;
}
@ -109,6 +113,7 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
deleteProjectMembers,
getProjectMembers,
setSearchQuery,
getSearchQuery,
setSortingBy,
setSortingDirection,
setPage,

View File

@ -2,6 +2,7 @@
// See LICENSE for copying information.
import AddTeamMemberModal from '@/components/modals/AddTeamMemberModal.vue';
import RemoveTeamMemberModal from '@/components/modals/RemoveProjectMemberModal.vue';
import EditProfileModal from '@/components/modals/EditProfileModal.vue';
import ChangePasswordModal from '@/components/modals/ChangePasswordModal.vue';
import CreateProjectModal from '@/components/modals/CreateProjectModal.vue';
@ -53,6 +54,7 @@ export const APP_STATE_DROPDOWNS = {
enum Modals {
ADD_TEAM_MEMBER = 'addTeamMember',
REMOVE_TEAM_MEMBER = 'removeTeamMember',
EDIT_PROFILE = 'editProfile',
CHANGE_PASSWORD = 'changePassword',
CREATE_PROJECT = 'createProject',
@ -85,6 +87,7 @@ enum Modals {
// modals could be of VueConstructor type or Object (for composition api components).
export const MODALS: Record<Modals, unknown> = {
[Modals.ADD_TEAM_MEMBER]: AddTeamMemberModal,
[Modals.REMOVE_TEAM_MEMBER]: RemoveTeamMemberModal,
[Modals.EDIT_PROFILE]: EditProfileModal,
[Modals.CHANGE_PASSWORD]: ChangePasswordModal,
[Modals.CREATE_PROJECT]: CreateProjectModal,

View File

@ -0,0 +1,7 @@
<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="M16.4423 0H23.3463C28.8835 0 31.0805 0.613722 33.2353 1.76616C35.3902 2.9186 37.0814 4.60976 38.2338 6.76464L38.3214 6.93054C39.4029 9.0067 39.9846 11.1999 40 16.4423V23.3463C40 28.8834 39.3863 31.0804 38.2338 33.2353C37.0814 35.3901 35.3902 37.0813 33.2353 38.2337L33.0694 38.3213C30.9933 39.4028 28.8 39.9845 23.5577 39.9999H16.6537C11.1165 39.9999 8.91954 39.3862 6.76466 38.2337C4.60977 37.0813 2.91861 35.3901 1.76617 33.2353L1.67858 33.0694C0.597074 30.9932 0.0154219 28.8 0 23.5576V16.6536C0 11.1165 0.613723 8.91952 1.76617 6.76464C2.91861 4.60976 4.60977 2.9186 6.76466 1.76616L6.93055 1.67858C9.00672 0.597072 11.2 0.0154218 16.4423 0Z" fill="#0218A7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.0143 30.6406C21.1472 30.6406 26.401 34.3892 28.591 39.7111C27.2939 39.8942 25.7013 39.9917 23.6294 39.9999L16.6537 40.0001C11.1165 40.0001 8.91953 39.3864 6.76464 38.2339C5.61182 37.6174 4.59172 36.8467 3.71582 35.9332C6.40599 32.7 10.4687 30.6406 15.0143 30.6406Z" fill="#FFC600"/>
<path d="M15.0413 28.0748C18.9762 28.0748 22.1661 24.8983 22.1661 20.9798C22.1661 17.0613 18.9762 13.8848 15.0413 13.8848C11.1064 13.8848 7.9165 17.0613 7.9165 20.9798C7.9165 24.8983 11.1064 28.0748 15.0413 28.0748Z" fill="#FF458B"/>
<path d="M24.9585 28.0748C28.8935 28.0748 32.0833 24.8983 32.0833 20.9798C32.0833 17.0613 28.8935 13.8848 24.9585 13.8848C21.0236 13.8848 17.8337 17.0613 17.8337 20.9798C17.8337 24.8983 21.0236 28.0748 24.9585 28.0748Z" fill="#FFC600"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.9317 30.6406C29.4915 30.6406 33.5653 32.7128 36.2553 35.9631C35.386 36.8635 34.3757 37.6241 33.2354 38.2339L33.0695 38.3215C31.0028 39.3981 28.8201 39.9794 23.6292 39.9999L16.6537 40.0001C14.4152 40.0001 12.7225 39.8998 11.3579 39.7042C13.5498 34.3858 18.8016 30.6406 24.9317 30.6406Z" fill="#FF458B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB