Files
speckle-server/packages/frontend-2/components/settings/workspaces/members/InvitesTable.vue
T
Mike 98cf564342 Feat: Expose user email on workspace settings member tables (#4857)
* Feat: Expose user email on workspace settings member tables

* Added permissions check

* Remove fetching items
2025-05-30 18:52:23 +02:00

188 lines
5.5 KiB
Vue

<template>
<div>
<SettingsWorkspacesMembersTableHeader
v-model:search="search"
search-placeholder="Search pending invites..."
:workspace="workspace"
/>
<LayoutTable
class="mt-6 md:mt-8 mb-12"
:columns="[
{ id: 'name', header: 'Name', classes: 'col-span-3' },
{ id: 'email', header: 'Email', classes: 'col-span-3' },
{ id: 'invitedBy', header: 'Invited by', classes: 'col-span-2' },
{ id: 'role', header: 'Role', classes: 'col-span-1' },
{ id: 'lastRemindedOn', header: 'Last reminded on', classes: 'col-span-2' },
{
id: 'actions',
header: '',
classes: 'col-span-1 flex items-center justify-end'
}
]"
:items="invites"
:loading="searchResultLoading"
:empty-message="
search.length
? 'No invites with the specified filter found'
: 'No pending invites'
"
>
<template #name="{ item }">
<div class="flex items-center gap-2">
<UserAvatar v-if="item.user" hide-tooltip :user="item.user" />
<span class="truncate text-body-xs text-foreground">{{ item.title }}</span>
</div>
</template>
<template #email="{ item }">
<div class="flex">
<span class="truncate text-body-xs text-foreground">{{ item.email }}</span>
</div>
</template>
<template #invitedBy="{ item }">
<div class="flex items-center gap-2">
<UserAvatar hide-tooltip :user="item.invitedBy" />
<span class="truncate text-body-xs text-foreground">
{{ item.invitedBy.name }}
</span>
</div>
</template>
<template #role="{ item }">
<span class="text-body-xs text-foreground-2">
{{ roleDisplayName(item.role) }}
</span>
</template>
<template #lastRemindedOn="{ item }">
<span class="text-body-xs text-foreground-2">
{{ formattedFullDate(item.updatedAt) }}
</span>
</template>
<template #actions="{ item }">
<LayoutMenu
v-model:open="showActionsMenu[item.id]"
:items="actionsItems"
mount-menu-on-body
:menu-position="HorizontalDirection.Left"
:menu-id="`invite-actions-${item.id}`"
@chosen="({ item: actionItem }) => onActionChosen(actionItem, item)"
>
<FormButton
:color="showActionsMenu[item.id] ? 'outline' : 'subtle'"
hide-text
:icon-right="showActionsMenu[item.id] ? XMarkIcon : EllipsisHorizontalIcon"
@click="toggleMenu(item.id)"
/>
</LayoutMenu>
</template>
</LayoutTable>
</div>
</template>
<script setup lang="ts">
import { XMarkIcon, EllipsisHorizontalIcon } from '@heroicons/vue/24/outline'
import { useQuery } from '@vue/apollo-composable'
import { capitalize } from 'lodash-es'
import { graphql } from '~/lib/common/generated/gql'
import type { SettingsWorkspacesMembersInvitesTable_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
import {
useCancelWorkspaceInvite,
useResendWorkspaceInvite
} from '~/lib/settings/composables/workspaces'
import { settingsWorkspacesInvitesSearchQuery } from '~/lib/settings/graphql/queries'
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
import { HorizontalDirection } from '~~/lib/common/composables/window'
import type { MaybeNullOrUndefined } from '@speckle/shared'
graphql(`
fragment SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {
id
inviteId
role
title
updatedAt
email
user {
id
...LimitedUserAvatar
}
invitedBy {
id
...LimitedUserAvatar
}
}
`)
graphql(`
fragment SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {
id
...SettingsWorkspacesMembersTableHeader_Workspace
invitedTeam {
...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator
}
}
`)
const props = defineProps<{
workspaceSlug: string
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersInvitesTable_WorkspaceFragment>
}>()
const search = ref('')
const showActionsMenu = ref<Record<string, boolean>>({})
const cancelInvite = useCancelWorkspaceInvite()
const resendInvite = useResendWorkspaceInvite()
const { result: searchResult, loading: searchResultLoading } = useQuery(
settingsWorkspacesInvitesSearchQuery,
() => ({
invitesFilter: {
search: search.value
},
slug: props.workspaceSlug
}),
() => ({
enabled: !!search.value.length
})
)
const invites = computed(() =>
search.value.length
? searchResult.value?.workspaceBySlug.invitedTeam
: props.workspace?.invitedTeam
)
const actionsItems: LayoutMenuItem[][] = [
[{ title: 'Resend invite', id: 'resend-invite' }],
[{ title: 'Delete invite', id: 'delete-invite' }]
]
const onActionChosen = async (
actionItem: LayoutMenuItem,
item: NonNullable<typeof invites.value>[0]
) => {
if (!props.workspace?.id) return
switch (actionItem.id) {
case 'resend-invite':
await resendInvite({
input: {
workspaceId: props.workspace.id,
inviteId: item.inviteId
}
})
break
case 'delete-invite':
await cancelInvite({
workspaceId: props.workspace.id,
inviteId: item.inviteId
})
break
}
}
const toggleMenu = (itemId: string) => {
showActionsMenu.value[itemId] = !showActionsMenu.value[itemId]
}
const roleDisplayName = (role: string) => capitalize(role.split(':')[1])
</script>