web/satellite/vuetify-poc: remove project member feature

Implemented remove project member functionality.
Also, fixed project members search/pagination/sort functionality to work through backend.

Issue:
https://github.com/storj/storj/issues/6327

Change-Id: I0a8df1578a8c7ab9b7d6ce8e2687a3a02cf6be57
This commit is contained in:
Vitalii 2023-09-26 14:57:48 +03:00 committed by Storj Robot
parent fd835859d5
commit eddfacc2e9
9 changed files with 507 additions and 26 deletions

View File

@ -13,7 +13,7 @@
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">
<div v-for="(member, key) in firstThreeSelected" :key="key" :title="member" class="modal__project-member">
{{ member }}
</div>
<div v-if="selectedMembersLength > 3" class="modal__project-member">
@ -241,6 +241,7 @@ function closeModal(): void {
&__project-member {
width: fit-content;
max-width: calc(100% - 40px);
text-align: left;
background: #f4f5f7;
border-radius: 30px;
@ -248,6 +249,9 @@ function closeModal(): void {
gap: 10px;
margin: 16px 0;
font-family: 'font_medium', sans-serif;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}

View File

@ -36,7 +36,12 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
return await api.getInviteLink(projectID, email);
}
async function deleteProjectMembers(projectID: string): Promise<void> {
async function deleteProjectMembers(projectID: string, customSelected?: string[]): Promise<void> {
if (customSelected && customSelected.length) {
await api.delete(projectID, customSelected);
return;
}
await api.delete(projectID, state.selectedProjectMembersEmails);
clearProjectMemberSelection();

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: 2.0 KiB

View File

@ -2,6 +2,11 @@
// See LICENSE for copying information.
<template>
<v-card min-height="24" :border="0" class="mb-2 elevation-0">
<v-row v-if="modelValue.length > 0" class="justify-end align-center ma-0">
<p>{{ modelValue.length }} user{{ modelValue.length !== 1 ? 's' : '' }} selected</p>
</v-row>
</v-card>
<v-card variant="flat" :border="true" rounded="xlg">
<v-text-field
v-model="search"
@ -17,16 +22,24 @@
class="mx-2 mt-2"
/>
<v-data-table
v-model="selected"
:sort-by="sortBy"
<v-data-table-server
v-model="model"
:search="search"
:headers="headers"
:items="projectMembers"
:loading="isLoading"
:items-length="page.totalCount"
:items-per-page-options="tableSizeOptions(page.totalCount)"
no-data-text="No results found"
:search="search"
class="elevation-1"
item-value="email"
select-strategy="all"
item-selectable="selectable"
show-select
hover
@update:itemsPerPage="onUpdateLimit"
@update:page="onUpdatePage"
@update:sortBy="onUpdateSortBy"
>
<template #item.name="{ item }">
<span class="font-weight-bold">
@ -38,25 +51,134 @@
{{ item.raw.role }}
</v-chip>
</template>
</v-data-table>
<template #item.actions="{ item }">
<v-btn
v-if="item.raw.role !== ProjectRole.Owner"
variant="outlined"
color="default"
size="small"
class="mr-1 text-caption"
density="comfortable"
icon
>
<v-icon icon="mdi-dots-horizontal" />
<v-menu activator="parent">
<v-list class="py-2">
<v-list-item
density="comfortable"
link
rounded="lg"
@click="() => onResendOrCopyClick(item.raw.expired, item.raw.email)"
>
<template #prepend>
<icon-upload v-if="item.raw.expired" :size="18" />
<icon-copy v-else />
</template>
<v-list-item-title class="pl-2 text-body-2 font-weight-medium">
{{ item.raw.expired ? 'Resend invite' : 'Copy invite link' }}
</v-list-item-title>
</v-list-item>
<v-list-item
density="comfortable"
link rounded="lg"
@click="() => onSingleDelete(item.raw.email)"
>
<template #prepend>
<icon-trash bold />
</template>
<v-list-item-title class="pl-2 text-body-2 font-weight-medium">
Remove member
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</template>
</v-data-table-server>
</v-card>
<remove-project-member-dialog
v-model="isRemoveMembersDialogShown"
:emails="modelValue"
@deleted="onPostDelete"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { VCard, VTextField, VChip } from 'vuetify/components';
import { VDataTable } from 'vuetify/labs/components';
import { computed, onMounted, ref, watch } from 'vue';
import {
VRow,
VCard,
VTextField,
VChip,
VIcon,
VList,
VMenu,
VListItem,
VBtn,
VListItemTitle,
} from 'vuetify/components';
import { VDataTableServer } from 'vuetify/labs/components';
import { useRouter } from 'vue-router';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { ProjectInvitationItemModel, ProjectRole } from '@/types/projectMembers';
import {
ProjectInvitationItemModel,
ProjectMemberCursor,
ProjectMemberOrderBy,
ProjectMembersPage,
ProjectRole,
} from '@/types/projectMembers';
import { PROJECT_ROLE_COLORS } from '@poc/types/projects';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useAppStore } from '@/store/modules/appStore';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
import { useLoading } from '@/composables/useLoading';
import { useNotify } from '@/utils/hooks';
import { Project } from '@/types/projects';
import { SortDirection, tableSizeOptions } from '@/types/common';
import { useUsersStore } from '@/store/modules/usersStore';
import IconTrash from '@poc/components/icons/IconTrash.vue';
import IconCopy from '@poc/components/icons/IconCopy.vue';
import IconUpload from '@poc/components/icons/IconUpload.vue';
import RemoveProjectMemberDialog from '@poc/components/dialogs/RemoveProjectMemberDialog.vue';
type RenderedItem = {
name: string,
email: string,
role: ProjectRole,
date: string,
selectable: boolean,
expired: boolean,
}
const usersStore = useUsersStore();
const analyticsStore = useAnalyticsStore();
const appStore = useAppStore();
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
const router = useRouter();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const emit = defineEmits<{
(event: 'update:modelValue', value: string[]): void,
}>();
const props = defineProps<{
modelValue: string[]
}>();
const model = computed<string[]>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const isRemoveMembersDialogShown = ref<boolean>(false);
const search = ref<string>('');
const selected = ref([]);
const searchTimer = ref<NodeJS.Timeout>();
const sortBy = ref([{ key: 'date', order: 'asc' }]);
const headers = ref([
{
@ -67,16 +189,24 @@ const headers = ref([
{ title: 'Email', key: 'email' },
{ title: 'Role', key: 'role' },
{ title: 'Date Added', key: 'date' },
{ title: '', key: 'actions', sortable: false, width: 0 },
]);
const selectedProject = computed<Project>(() => projectsStore.state.selectedProject);
const cursor = computed<ProjectMemberCursor>(() => pmStore.state.cursor);
const page = computed<ProjectMembersPage>(() => pmStore.state.page as ProjectMembersPage);
const FIRST_PAGE = 1;
const inviteLinksCache = new Map<string, string>();
/**
* Returns team members of current page from store.
* With project owner pinned to top
*/
const projectMembers = computed(() => {
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);
const projectMembers = computed((): RenderedItem[] => {
const projectMembers = page.value.getAllItems();
const projectOwner = projectMembers.find((member) => member.getUserID() === selectedProject.value.ownerId);
const projectMembersToReturn = projectMembers.filter((member) => member.getUserID() !== selectedProject.value.ownerId);
// if the project owner exists, place at the front of the members list
projectOwner && projectMembersToReturn.unshift(projectOwner);
@ -98,7 +228,145 @@ const projectMembers = computed(() => {
email: member.getEmail(),
role,
date: member.getJoinDate().toLocaleDateString('en-US', { day:'numeric', month:'short', year:'numeric' }),
selectable: role !== ProjectRole.Owner,
expired: member.isPending() && 'expired' in member && Boolean(member.expired),
};
});
});
/**
* Handles update table rows limit event.
*/
async function onUpdateLimit(limit: number): Promise<void> {
await fetch(page.value.currentPage, limit);
}
/**
* Handles update table page event.
*/
async function onUpdatePage(page: number): Promise<void> {
await fetch(page, cursor.value.limit);
}
/**
* Handles post delete operations.
*/
async function onPostDelete(): Promise<void> {
if (props.modelValue.includes(usersStore.state.user.email)) {
router.push('/projects');
return;
}
search.value = '';
emit('update:modelValue', []);
await onUpdatePage(FIRST_PAGE);
}
function onSingleDelete(email: string): void {
emit('update:modelValue', [email]);
isRemoveMembersDialogShown.value = true;
}
/**
* Handles update table sorting event.
*/
async function onUpdateSortBy(sortBy: {key: keyof ProjectMemberOrderBy, order: keyof SortDirection}[]): Promise<void> {
if (!sortBy.length) return;
const sorting = sortBy[0];
pmStore.setSortingBy(ProjectMemberOrderBy[sorting.key]);
pmStore.setSortingDirection(SortDirection[sorting.order]);
await fetch(FIRST_PAGE, cursor.value.limit);
}
/**
* Handles on invite raw action click logic depending on expiration status.
*/
async function onResendOrCopyClick(expired: boolean, email: string): Promise<void> {
expired ? await resendInvite(email) : await copyInviteLink(email);
}
/**
* Resends project invitation to current project.
*/
async function resendInvite(email: string): Promise<void> {
await withLoading(async () => {
analyticsStore.eventTriggered(AnalyticsEvent.RESEND_INVITE_CLICKED);
try {
await pmStore.inviteMembers([email], selectedProject.value.id);
notify.notify('Invite resent!');
} catch (error) {
error.message = `Error resending invite. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_MEMBERS_PAGE);
return;
}
await onUpdatePage(FIRST_PAGE);
});
}
/**
* Copies project invitation link to user's clipboard.
*/
async function copyInviteLink(email: string): Promise<void> {
analyticsStore.eventTriggered(AnalyticsEvent.COPY_INVITE_LINK_CLICKED);
const cachedLink = inviteLinksCache.get(email);
if (cachedLink) {
await navigator.clipboard.writeText(cachedLink);
notify.notify('Invite copied!');
return;
}
await withLoading(async () => {
try {
const link = await pmStore.getInviteLink(email, selectedProject.value.id);
await navigator.clipboard.writeText(link);
inviteLinksCache.set(email, link);
notify.notify('Invite copied!');
} catch (error) {
error.message = `Error getting invite link. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_MEMBERS_PAGE);
}
});
}
/**
* Fetches Project members records depending on page and limit.
*/
async function fetch(page = FIRST_PAGE, limit = DEFAULT_PAGE_LIMIT): Promise<void> {
await withLoading(async () => {
try {
await pmStore.getProjectMembers(page, selectedProject.value.id, limit);
} catch (error) {
notify.error(`Unable to fetch Project Members. ${error.message}`, AnalyticsErrorEventSource.PROJECT_MEMBERS_PAGE);
}
});
}
/**
* Makes delete project members dialog visible.
*/
function showDeleteDialog(): void {
isRemoveMembersDialogShown.value = true;
}
/**
* Handles update table search.
*/
watch(() => search.value, () => {
clearTimeout(searchTimer.value);
searchTimer.value = setTimeout(() => {
pmStore.setSearchQuery(search.value || '');
fetch();
}, 500); // 500ms delay for every new call.
});
onMounted(() => {
fetch();
});
defineExpose({ showDeleteDialog });
</script>

View File

@ -0,0 +1,160 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="410px"
transition="fade-transition"
:persistent="isLoading"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-sheet
class="bg-on-surface-variant d-flex justify-center align-center"
width="40"
height="40"
rounded="lg"
>
<img src="@poc/assets/icon-remove-member.svg" alt="member icon">
</v-sheet>
</template>
<v-card-title class="font-weight-bold">Remove member</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isLoading"
@click="model = false"
/>
</template>
</v-card-item>
<v-card-item class="px-7 py-0">
<v-divider />
<p class="py-4">The following team members will be removed. This action cannot be undone.</p>
<v-divider />
</v-card-item>
<v-card-item class="px-7 pt-4 pb-1">
<v-chip
v-for="email in firstThreeSelected"
:key="email"
rounded
class="mb-3"
>
<template #default>
<div class="max-width">
<p :title="email" class="text-truncate">{{ email }}</p>
</div>
</template>
</v-chip>
<v-chip v-if="props.emails.length > 3" rounded class="mb-3">
+ {{ props.emails.length - 3 }} more
</v-chip>
</v-card-item>
<v-card-item class="px-7 py-0">
<v-card class="mb-4 pa-4" color="warning">
<p>
<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.
</p>
</v-card>
<v-divider />
</v-card-item>
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn variant="outlined" color="default" block :disabled="isLoading" @click="model = false">
Cancel
</v-btn>
</v-col>
<v-col>
<v-btn color="error" variant="flat" block :loading="isLoading" @click="onDelete">
Delete
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import {
VDialog,
VCard,
VCardItem,
VSheet,
VCardTitle,
VDivider,
VCardActions,
VRow,
VCol,
VBtn,
VChip,
} from 'vuetify/components';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useLoading } from '@/composables/useLoading';
import { useNotify } from '@/utils/hooks';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
const props = defineProps<{
modelValue: boolean,
emails: string[],
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean],
'deleted': [];
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const analyticsStore = useAnalyticsStore();
const configStore = useConfigStore();
const projectsStore = useProjectsStore();
const pmStore = useProjectMembersStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const firstThreeSelected = computed<string[]>(() => props.emails.slice(0, 3));
async function onDelete(): Promise<void> {
await withLoading(async () => {
try {
await pmStore.deleteProjectMembers(projectsStore.state.selectedProject.id, props.emails);
notify.success('Members were successfully removed from the project');
emit('deleted');
model.value = false;
} catch (error) {
error.message = `Error removing project members. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_MEMBERS_HEADER);
}
});
}
</script>
<style scoped lang="scss">
.max-width {
max-width: 320px;
}
</style>

View File

@ -0,0 +1,17 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<svg :width="size" :height="size" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.96896 3.46521C3.1732 3.70087 2.59436 4.41876 2.59436 5.26774V13.4991C2.59436 14.5398 3.46409 15.3834 4.53696 15.3834H10.5693C11.4445 15.3834 12.1846 14.8219 12.4275 14.05H11.1578C11.0091 14.1995 10.8004 14.2925 10.5693 14.2925H4.53696L4.51149 14.2921C4.07154 14.279 3.71903 13.929 3.71903 13.4991V5.26774L3.71941 5.24303C3.72617 5.02872 3.82055 4.83579 3.96896 4.69682V3.46521Z" fill="#56606D" />
<path d="M10.117 1.38342C10.4424 1.38342 10.7545 1.5088 10.9846 1.73198L14.2144 4.86481C14.4445 5.088 14.5738 5.3907 14.5738 5.70634V11.4991C14.5738 12.5398 13.7041 13.3834 12.6312 13.3834H6.59891C5.52604 13.3834 4.65631 12.5398 4.65631 11.4991V3.26779C4.65631 2.22712 5.52604 1.38342 6.59891 1.38342H10.117ZM9.61496 2.47432H6.59891C6.15573 2.47432 5.79483 2.8163 5.78137 3.24307L5.78098 3.26779V11.4991C5.78098 11.929 6.13349 12.2791 6.57344 12.2921L6.59891 12.2925H12.6312C13.0744 12.2925 13.4353 11.9506 13.4488 11.5239L13.4491 11.4991V6.19331L10.1774 6.19339C9.8744 6.19339 9.62738 5.96095 9.61551 5.66987L9.61506 5.64794L9.61496 2.47432ZM12.8689 5.10241L10.7396 3.03713L10.7397 5.10249L12.8689 5.10241Z" fill="#56606D" />
</svg>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
size: number;
}>(), {
size: 18,
});
</script>

View File

@ -2,16 +2,17 @@
// See LICENSE for copying information.
<template>
<!-- Upload Icon -->
<svg width="16" height="16" viewBox="0 0 14 14" fill="none" class="mr-2" xmlns="http://www.w3.org/2000/svg">
<svg :width="size" :height="size" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path :fill="color" d="M7.28436 0.852711L7.30105 0.868755L10.2606 3.82831C10.4854 4.05305 10.4854 4.41744 10.2606 4.64219C10.0413 4.86145 9.68917 4.8668 9.46342 4.65823L9.44673 4.64219L7.45038 2.6458V9.14788C7.45038 9.46572 7.19272 9.72338 6.87489 9.72338C6.5648 9.72338 6.31199 9.47813 6.29984 9.17103L6.29939 9.14788V2.6841L4.3415 4.64219C4.12224 4.86145 3.77006 4.8668 3.54431 4.65823L3.52762 4.64219C3.30836 4.42292 3.30301 4.07075 3.51158 3.845L3.52762 3.82831L6.48718 0.868755C6.70644 0.64949 7.05862 0.644143 7.28436 0.852711ZM13.2992 12.1406C13.2992 12.4584 13.0416 12.7161 12.7237 12.7161H1.3184C1.00056 12.7161 0.742899 12.4584 0.742899 12.1406C0.742899 11.8227 1.00056 11.5651 1.3184 11.5651H12.7237C13.0416 11.5651 13.2992 11.8227 13.2992 12.1406ZM1.27472 12.6724C0.956878 12.6724 0.699219 12.4147 0.699219 12.0969V9.79489C0.699219 9.47705 0.956878 9.21939 1.27472 9.21939C1.59256 9.21939 1.85021 9.47705 1.85021 9.79489V12.0969C1.85021 12.4147 1.59256 12.6724 1.27472 12.6724ZM12.68 12.6724C12.3622 12.6724 12.1045 12.4147 12.1045 12.0969V9.79489C12.1045 9.47705 12.3622 9.21939 12.68 9.21939C12.9979 9.21939 13.2555 9.47705 13.2555 9.79489V12.0969C13.2555 12.4147 12.9979 12.6724 12.68 12.6724Z" />
</svg>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
size: number;
color: string;
}>(), {
size: 16,
color: 'currentColor',
});
</script>

View File

@ -21,7 +21,7 @@
v-bind="props"
>
<browser-snackbar-component v-model="isObjectsUploadModal" />
<IconUpload />
<IconUpload class="mr-2" />
Upload
</v-btn>
</template>

View File

@ -9,8 +9,8 @@
link="https://docs.storj.io/support/users"
/>
<v-col>
<v-row class="mt-2 mb-4">
<v-col class="pb-0">
<v-row class="mt-2 mb-0">
<v-btn>
<svg width="16" height="16" class="mr-2" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1ZM10 2.65C5.94071 2.65 2.65 5.94071 2.65 10C2.65 14.0593 5.94071 17.35 10 17.35C14.0593 17.35 17.35 14.0593 17.35 10C17.35 5.94071 14.0593 2.65 10 2.65ZM10.7496 6.8989L10.7499 6.91218L10.7499 9.223H12.9926C13.4529 9.223 13.8302 9.58799 13.8456 10.048C13.8602 10.4887 13.5148 10.8579 13.0741 10.8726L13.0608 10.8729L10.7499 10.873L10.75 13.171C10.75 13.6266 10.3806 13.996 9.925 13.996C9.48048 13.996 9.11807 13.6444 9.10066 13.2042L9.1 13.171L9.09985 10.873H6.802C6.34637 10.873 5.977 10.5036 5.977 10.048C5.977 9.60348 6.32857 9.24107 6.76882 9.22366L6.802 9.223H9.09985L9.1 6.98036C9.1 6.5201 9.46499 6.14276 9.925 6.12745C10.3657 6.11279 10.7349 6.45818 10.7496 6.8989Z" fill="currentColor" />
@ -110,15 +110,25 @@
</v-card>
</v-dialog>
</v-btn>
<v-btn
v-if="selectedMembers.length"
prepend-icon="mdi-trash-can-outline"
variant="outlined"
color="default"
class="ml-2 text-caption"
@click="showDeleteDialog"
>
Remove
</v-btn>
</v-row>
</v-col>
<TeamTableComponent />
<TeamTableComponent ref="tableComponent" v-model="selectedMembers" />
</v-container>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { computed, ref } from 'vue';
import {
VContainer,
VCol,
@ -145,6 +155,10 @@ import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import PageSubtitleComponent from '@poc/components/PageSubtitleComponent.vue';
import TeamTableComponent from '@poc/components/TeamTableComponent.vue';
interface DeleteDialog {
showDeleteDialog(): void;
}
const analyticsStore = useAnalyticsStore();
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
@ -154,6 +168,8 @@ const isLoading = ref<boolean>(false);
const dialog = ref<boolean>(false);
const valid = ref<boolean>(false);
const email = ref<string>('');
const selectedMembers = ref<string[]>([]);
const tableComponent = ref<TeamTableComponent & DeleteDialog>();
const emailRules = [
(value: string): string | boolean => (!!value || 'E-mail is requred.'),
@ -194,7 +210,10 @@ async function onAddUsersClick(): Promise<void> {
isLoading.value = false;
}
onMounted(() => {
pmStore.getProjectMembers(1, selectedProjectID.value);
});
/**
* Makes delete project members dialog visible.
*/
function showDeleteDialog(): void {
tableComponent.value.showDeleteDialog();
}
</script>