Feat: Cleanup invites (#4310)
This commit is contained in:
@@ -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
Reference in New Issue
Block a user