Files
speckle-server/packages/frontend-2/components/invite/dialog/Server.vue
T
2025-01-06 13:30:39 +01:00

196 lines
5.9 KiB
Vue

<template>
<LayoutDialog v-model:open="isOpen" max-width="md" :buttons="dialogButtons">
<template #header>Invite to Speckle</template>
<form @submit="onSubmit">
<div class="flex flex-col gap-y-5 text-foreground">
<div v-for="(item, index) in fields" :key="item.key" class="flex gap-x-3">
<div class="flex flex-col gap-y-3 flex-1">
<hr v-if="index !== 0" class="border-outline-3" />
<div class="flex flex-row gap-x-3 items-center">
<div class="flex-1">
<FormTextInput
v-model="item.value.email"
:name="`email-${item.key}`"
color="foundation"
placeholder="Email address"
show-clear
full-width
use-label-in-errors
show-label
label="Email"
:rules="[isEmail]"
/>
</div>
<FormSelectServerRoles
v-if="allowServerRoleSelect"
v-model="item.value.serverRole"
label="Select role"
:name="`role-${item.key}`"
class="sm:w-48"
show-label
:disabled="anyMutationsLoading"
:allow-guest="isGuestMode"
:allow-admin="isAdmin"
mount-menu-on-body
/>
</div>
<FormSelectProjects
v-model="item.value.project"
label="Select project"
class="w-full"
owned-only
show-optional
mount-menu-on-body
show-label
:name="`project-${index}`"
/>
</div>
<div class="relative w-4">
<CommonTextLink
v-if="fields.length > 1"
class="top-10 absolute right-0"
:class="{ 'top-7': index === 0 }"
@click="removeInviteItem(index)"
>
<TrashIcon class="h-4 w-4 text-foreground-2" />
</CommonTextLink>
</div>
</div>
<FormButton
color="subtle"
:icon-left="PlusIcon"
:disabled="anyMutationsLoading"
@click="addInviteItem"
>
Invite another user
</FormButton>
</div>
</form>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { useForm, useFieldArray } from 'vee-validate'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useServerInfo } from '~~/lib/core/composables/server'
import { useInviteUserToProject } from '~~/lib/projects/composables/projectManagement'
import { useInviteUserToServer } from '~~/lib/server/composables/invites'
import { PlusIcon, TrashIcon } from '@heroicons/vue/24/outline'
import type { InviteServerForm, InviteServerItem } from '~~/lib/invites/helpers/types'
import { emptyInviteServerItem } from '~~/lib/invites/helpers/constants'
import { isEmail } from '~~/lib/common/helpers/validation'
import { useGlobalToast } from '~~/lib/common/composables/toast'
const isOpen = defineModel<boolean>('open', { required: true })
const { handleSubmit } = useForm<InviteServerForm>({
initialValues: {
fields: [
{
...emptyInviteServerItem
}
]
}
})
const {
fields,
replace: replaceFields,
push: pushInvite,
remove: removeInvite
} = useFieldArray<InviteServerItem>('fields')
const { mutate: inviteUserToServer } = useInviteUserToServer()
const inviteUserToProject = useInviteUserToProject()
const anyMutationsLoading = useMutationLoading()
const { isAdmin } = useActiveUser()
const { isGuestMode } = useServerInfo()
const mixpanel = useMixpanel()
const { triggerNotification } = useGlobalToast()
const allowServerRoleSelect = computed(() => isAdmin.value || isGuestMode.value)
const dialogButtons = computed((): LayoutDialogButton[] => [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => {
isOpen.value = false
}
},
{
text: 'Invite',
props: {
submit: true,
disabled: anyMutationsLoading.value
},
onClick: onSubmit
}
])
const addInviteItem = () => {
pushInvite({ ...emptyInviteServerItem })
}
const removeInviteItem = (index: number) => {
removeInvite(index)
}
const onSubmit = handleSubmit(async () => {
const invites = fields.value.filter((invite) => invite.value.email)
try {
await Promise.all(
invites.map((invite) =>
invite.value.project
? inviteUserToProject(
invite.value.project.id,
[{ email: invite.value.email, serverRole: invite.value.serverRole }],
{ hideToasts: true }
)
: inviteUserToServer(
[{ email: invite.value.email, serverRole: invite.value.serverRole }],
{ hideToasts: true }
)
)
)
triggerNotification({
type: ToastNotificationType.Success,
title:
invites.length > 1
? 'Invites successfully send'
: `Invite successfully sent to ${invites[0].value.email}`
})
mixpanel.track('Invite Action', {
type: 'server invite',
name: 'send',
multiple: fields.value.length !== 1,
count: fields.value.length,
hasProject: !!fields.value.some((invite) => invite.value.project),
to: 'email'
})
isOpen.value = false
} catch {
triggerNotification({
type: ToastNotificationType.Danger,
title:
invites.length > 1
? 'One or more invites failed to send'
: `Failed to send invite to ${invites[0].value.email}`
})
}
})
watch(isOpen, (newVal, oldVal) => {
if (newVal && !oldVal) {
replaceFields([
{
...emptyInviteServerItem
}
])
}
})
</script>