Feat: Cleanup invites (#4310)

This commit is contained in:
Mike
2025-04-02 17:01:28 +02:00
committed by GitHub
parent d752bcb274
commit 03560111bd
5 changed files with 68 additions and 186 deletions
@@ -16,21 +16,10 @@
ref="selectUsers"
:invites="invites"
:allowed-domains="allowedDomains"
:show-workspace-roles="!isWorkspaceNewPlansEnabled"
>
<p v-if="showBillingInfo" class="text-body-2xs text-foreground-2 leading-5">
Inviting users may add seats to your current billing cycle. If there are
available seats, they will be used first. Your workspace is currently billed for
{{ memberSeatText }}{{ hasGuestSeats ? ` and ${guestSeatText}` : '' }}.
<p class="text-body-2xs text-foreground-2 leading-5">
{{ infoText }}
</p>
<template #info>
<p
v-if="isWorkspaceNewPlansEnabled"
class="text-body-2xs text-foreground-2 leading-5"
>
{{ infoText }}
</p>
</template>
</InviteDialogSharedSelectUsers>
</LayoutDialog>
</template>
@@ -38,11 +27,9 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { graphql } from '~/lib/common/generated/gql'
import {
type InviteDialogWorkspace_WorkspaceFragment,
type WorkspaceInviteCreateInput,
type WorkspacePlans,
WorkspacePlanStatuses
import type {
InviteDialogWorkspace_WorkspaceFragment,
WorkspaceInviteCreateInput
} from '~/lib/common/generated/gql/graphql'
import type { InviteWorkspaceItem } from '~~/lib/invites/helpers/types'
import { emptyInviteWorkspaceItem } from '~~/lib/invites/helpers/constants'
@@ -51,7 +38,6 @@ import { useMixpanel } from '~/lib/core/composables/mp'
import { mapMainRoleToGqlWorkspaceRole } from '~/lib/workspaces/helpers/roles'
import { mapServerRoleToGqlServerRole } from '~/lib/common/helpers/roles'
import { useInviteUserToWorkspace } from '~/lib/workspaces/composables/management'
import { isPaidPlan } from '~/lib/billing/helpers/types'
import { getRoleLabel } from '~~/lib/settings/helpers/utils'
import { matchesDomainPolicy } from '~/lib/invites/helpers/validation'
@@ -64,16 +50,6 @@ graphql(`
domain
id
}
plan {
status
name
}
subscription {
seats {
guest
plan
}
}
}
`)
@@ -84,7 +60,6 @@ const isOpen = defineModel<boolean>('open', { required: true })
const mixpanel = useMixpanel()
const inviteToWorkspace = useInviteUserToWorkspace()
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const isSelectingRole = ref(true)
const selectedRole = ref<WorkspaceRoles>(Roles.Workspace.Member)
@@ -111,23 +86,14 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
}
])
const title = computed(() => {
if (isWorkspaceNewPlansEnabled.value) {
return isSelectingRole.value
? 'Who are you inviting to the workspace?'
: `Invite ${getRoleLabel(
selectedRole.value
).title.toLowerCase()}s to the workspace`
}
return 'Invite to Workspace'
})
const title = computed(() =>
isSelectingRole.value
? 'Who are you inviting to the workspace?'
: `Invite ${getRoleLabel(selectedRole.value).title.toLowerCase()}s to the workspace`
)
const backButtonText = computed(() =>
isWorkspaceNewPlansEnabled.value && !isSelectingRole.value ? 'Back' : 'Cancel'
)
const nextButtonText = computed(() =>
isWorkspaceNewPlansEnabled.value && isSelectingRole.value ? 'Continue' : 'Invite'
)
const backButtonText = computed(() => (isSelectingRole.value ? 'Cancel' : 'Back'))
const nextButtonText = computed(() => (isSelectingRole.value ? 'Continue' : 'Invite'))
const allowedDomains = computed(() =>
props.workspace?.domainBasedMembershipProtectionEnabled
? props.workspace.domains?.map((d) => d.domain)
@@ -140,40 +106,9 @@ const infoText = computed(() => {
return `They don't work at ${props.workspace?.name}. They can collaborate on projects but can't create projects, invite others, add people, or be admins.`
})
// TODO: All of these billing info will not be used in the new flow, needs to be removed post-release
const memberSeatText = computed(() => {
if (!props.workspace?.subscription) return ''
return `${props.workspace.subscription.seats.plan} member ${
props.workspace.subscription.seats.plan === 1 ? 'seat' : 'seats'
}`
})
const guestSeatText = computed(() => {
if (!props.workspace?.subscription) return ''
return `${props.workspace.subscription.seats.guest} guest ${
props.workspace.subscription.seats.guest === 1 ? 'seat' : 'seats'
}`
})
const hasGuestSeats = computed(() => {
return (
props.workspace?.subscription?.seats.guest &&
props.workspace.subscription.seats.guest > 0
)
})
const showBillingInfo = computed(() => {
if (
!props.workspace?.plan ||
!props.workspace?.subscription ||
isWorkspaceNewPlansEnabled.value
)
return false
return (
isPaidPlan(props.workspace.plan.name as unknown as WorkspacePlans) &&
props.workspace.plan.status === WorkspacePlanStatuses.Valid
)
})
const onBack = () => {
if (isSelectingRole.value || !isWorkspaceNewPlansEnabled.value) {
if (isSelectingRole.value) {
isOpen.value = false
} else {
isSelectingRole.value = true
@@ -197,13 +132,9 @@ const onSelectUsersSubmit = async (updatedInvites: InviteWorkspaceItem[]) => {
invites.value = updatedInvites
const inputs: WorkspaceInviteCreateInput[] = invites.value.map((invite) => ({
role: isWorkspaceNewPlansEnabled.value
? canBeMember(invite.email)
? mapMainRoleToGqlWorkspaceRole(selectedRole.value)
: mapMainRoleToGqlWorkspaceRole(Roles.Workspace.Guest)
: invite.workspaceRole
? mapMainRoleToGqlWorkspaceRole(invite.workspaceRole)
: undefined,
role: canBeMember(invite.email)
? mapMainRoleToGqlWorkspaceRole(selectedRole.value)
: mapMainRoleToGqlWorkspaceRole(Roles.Workspace.Guest),
email: invite.email,
serverRole: invite.serverRole
? mapServerRoleToGqlServerRole(invite.serverRole)
@@ -227,8 +158,7 @@ const onSelectUsersSubmit = async (updatedInvites: InviteWorkspaceItem[]) => {
watch(isOpen, (newVal) => {
if (newVal) {
// Only show the first step for new plans
isSelectingRole.value = isWorkspaceNewPlansEnabled.value
isSelectingRole.value = true
invites.value = [
{
...emptyInviteWorkspaceItem,
@@ -62,16 +62,6 @@
</FormButton>
</div>
</form>
<div
v-if="showBillingInfo"
class="text-body-2xs text-foreground-2 leading-5 mt-4"
>
<p>
Inviting users may add seats to your current billing cycle. Your workspace is
currently billed for
{{ memberSeatText }}{{ hasGuestSeats ? ` and ${guestSeatText}` : '' }}.
</p>
</div>
</template>
</LayoutDialog>
</template>
@@ -84,15 +74,12 @@ import type { InviteProjectForm, InviteProjectItem } from '~~/lib/invites/helper
import { emptyInviteProjectItem } from '~~/lib/invites/helpers/constants'
import { isEmailOrEmpty } from '~~/lib/common/helpers/validation'
import { Roles } from '@speckle/shared'
import {
type InviteDialogProject_ProjectFragment,
type WorkspacePlans,
type ProjectInviteCreateInput,
type WorkspaceProjectInviteCreateInput,
WorkspacePlanStatuses
import type {
InviteDialogProject_ProjectFragment,
ProjectInviteCreateInput,
WorkspaceProjectInviteCreateInput
} from '~/lib/common/generated/gql/graphql'
import { useTeamInternals } from '~~/lib/projects/composables/team'
import { isPaidPlan } from '~/lib/billing/helpers/types'
import { useInviteUserToProject } from '~~/lib/projects/composables/projectManagement'
import { useMixpanel } from '~~/lib/core/composables/mp'
@@ -110,16 +97,6 @@ graphql(`
domain
id
}
plan {
status
name
}
subscription {
seats {
guest
plan
}
}
}
}
`)
@@ -161,26 +138,6 @@ const invitableWorkspaceMembers = computed(() => {
)
})
const isInWorkspace = computed(() => !!props.project.workspace?.id)
const memberSeatText = computed(() =>
props.project.workspace?.subscription?.seats.plan
? getSeatText(props.project.workspace.subscription.seats.plan, 'member')
: ''
)
const guestSeatText = computed(() =>
props.project.workspace?.subscription?.seats.guest
? getSeatText(props.project.workspace.subscription.seats.guest, 'guest')
: ''
)
const hasGuestSeats = computed(
() => (props.project.workspace?.subscription?.seats.guest ?? 0) > 0
)
const showBillingInfo = computed(() => {
if (!props.project.workspace?.plan) return false
return (
isPaidPlan(props.project.workspace.plan.name as unknown as WorkspacePlans) &&
props.project.workspace.plan.status === WorkspacePlanStatuses.Valid
)
})
const isAdmin = computed(() => props.project.workspace?.role === Roles.Workspace.Admin)
const dialogButtons = computed((): LayoutDialogButton[] => [
{
@@ -201,9 +158,6 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
: [])
])
const getSeatText = (count: number, type: 'member' | 'guest') =>
`${count} ${type} ${count === 1 ? 'seat' : 'seats'}`
const addInviteItem = () => {
pushInvite({
...emptyInviteProjectItem,
@@ -1,7 +1,7 @@
<template>
<form>
<div class="flex flex-col gap-y-3 text-foreground">
<slot name="info" />
<slot />
<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">
@@ -59,8 +59,6 @@
<FormButton color="subtle" :icon-left="PlusIcon" @click="addInviteItem">
Add another user
</FormButton>
<slot />
</div>
</form>
</template>
@@ -53,8 +53,8 @@ type Documents = {
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": typeof types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembers_Project on Project {\n id\n ...ProjectPageTeamInternals_Project\n workspace {\n team {\n items {\n ...InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator\n }\n }\n }\n }\n": typeof types.InviteDialogProjectWorkspaceMembers_ProjectFragmentDoc,
"\n fragment ProjectModelPageHeaderProject on Project {\n id\n name\n model(id: $modelId) {\n id\n name\n description\n }\n workspace {\n id\n slug\n name\n }\n }\n": typeof types.ProjectModelPageHeaderProjectFragmentDoc,
@@ -459,8 +459,8 @@ const documents: Documents = {
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembers_Project on Project {\n id\n ...ProjectPageTeamInternals_Project\n workspace {\n team {\n items {\n ...InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator\n }\n }\n }\n }\n": types.InviteDialogProjectWorkspaceMembers_ProjectFragmentDoc,
"\n fragment ProjectModelPageHeaderProject on Project {\n id\n name\n model(id: $modelId) {\n id\n name\n description\n }\n workspace {\n id\n slug\n name\n }\n }\n": types.ProjectModelPageHeaderProjectFragmentDoc,
@@ -999,11 +999,11 @@ export function graphql(source: "\n fragment HeaderWorkspaceSwitcherHeaderWorks
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n"): (typeof documents)["\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n"];
export function graphql(source: "\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n"): (typeof documents)["\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n"): (typeof documents)["\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n"];
export function graphql(source: "\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n"): (typeof documents)["\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long