Skip to content

Commit

Permalink
Feat: Billing info on change role (#3715)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikehrn authored Dec 18, 2024
1 parent f50a95f commit 047f21a
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 117 deletions.
9 changes: 9 additions & 0 deletions packages/frontend-2/components/projects/DeleteDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,13 @@ const dialogButtons = computed<LayoutDialogButton[]>(() => [
}
}
])
watch(
() => isOpen.value,
(newVal, oldVal) => {
if (newVal && !oldVal) {
projectNameInput.value = ''
}
}
)
</script>
106 changes: 0 additions & 106 deletions packages/frontend-2/components/settings/shared/ChangeRoleDialog.vue

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<template>
<LayoutDialog v-model:open="open" max-width="sm" :buttons="dialogButtons">
<template #header>Change role</template>
<div class="flex flex-col gap-4 mb-4 -mt-1">
<FormSelectWorkspaceRoles
v-model="newRole"
label="New role"
fully-control-value
:disabled-items="disabledItems"
:current-role="currentRole"
show-label
show-description
/>
<div
v-if="
workspaceDomainPolicyCompliant === false && newRole !== Roles.Workspace.Guest
"
class="flex gap-x-2 items-center"
>
<ExclamationCircleIcon class="text-danger w-4 h-4" />
<p class="text-foreground">
This user can only have the guest role due to the workspace policy.
</p>
</div>
<CommonCard
v-if="newRole"
class="bg-foundation !py-4 text-body-2xs flex flex-row gap-y-2"
>
<p
v-for="(message, i) in getWorkspaceProjectRoleMessages(newRole)"
:key="`message-${i}`"
>
{{ message }}
</p>
</CommonCard>
<div v-if="showBillingInfo" class="text-body-2xs text-foreground-2 leading-5">
<p class="mb-2">
Your workspace is currently billed for {{ memberSeatText
}}{{ hasGuestSeats ? ` and ${guestSeatText}` : '' }}.
<br />
Changing a user's role may add a seat to your current billing cycle.
</p>
<p>
Released seats will be adjusted at the start of your next billing cycle:
<br />
{{ nextBillingCycleEnd }}
</p>
</div>
</div>
</LayoutDialog>
</template>

<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { type MaybeNullOrUndefined, Roles, type WorkspaceRoles } from '@speckle/shared'
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
import { graphql } from '~/lib/common/generated/gql'
import {
type SettingsWorkspacesMembersChangeRoleDialog_WorkspaceFragment,
type WorkspacePlans,
WorkspacePlanStatuses
} from '~/lib/common/generated/gql/graphql'
import dayjs from 'dayjs'
import { isPaidPlan } from '~/lib/billing/helpers/types'
graphql(`
fragment SettingsWorkspacesMembersChangeRoleDialog_Workspace on Workspace {
id
plan {
status
name
}
subscription {
currentBillingCycleEnd
seats {
guest
plan
}
}
}
`)
const emit = defineEmits<{
(e: 'updateRole', newRole: WorkspaceRoles): void
}>()
const props = defineProps<{
workspaceDomainPolicyCompliant?: boolean | null
currentRole?: WorkspaceRoles
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersChangeRoleDialog_WorkspaceFragment>
}>()
const open = defineModel<boolean>('open', { required: true })
const newRole = ref<WorkspaceRoles | undefined>()
const disabledItems = computed<WorkspaceRoles[]>(() =>
props.workspaceDomainPolicyCompliant === false
? [Roles.Workspace.Member, Roles.Workspace.Admin]
: []
)
const dialogButtons = computed((): LayoutDialogButton[] => [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => (open.value = false)
},
{
text: 'Update',
props: { color: 'primary', disabled: !newRole.value },
onClick: () => {
open.value = false
if (newRole.value) {
emit('updateRole', newRole.value)
}
}
}
])
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 nextBillingCycleEnd = computed(() => {
if (!props.workspace?.subscription) return ''
return dayjs(props.workspace.subscription.currentBillingCycleEnd).format(
'MMMM D, YYYY'
)
})
const showBillingInfo = computed(() => {
if (!props.workspace?.plan || !newRole.value) return false
return (
isPaidPlan(props.workspace.plan.name as unknown as WorkspacePlans) &&
props.workspace.plan.status === WorkspacePlanStatuses.Valid
)
})
const getWorkspaceProjectRoleMessages = (workspaceRole: WorkspaceRoles): string[] => {
switch (workspaceRole) {
case Roles.Workspace.Admin:
return [
'Becomes project owner for all existing and new workspace projects.',
'Cannot be removed or have role changed by project owners.'
]
case Roles.Workspace.Member:
return [
'Becomes project viewer for all existing and new workspace projects.',
'Project owners can change their role or remove them.'
]
case Roles.Workspace.Guest:
return [
'Loses access to all existing workspace projects.',
'Project owners can assign a role or remove them.'
]
}
}
watch(
() => open.value,
(newVal, oldVal) => {
if (newVal && !oldVal) {
newRole.value = undefined
}
}
)
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@
:workspace-id="workspaceId"
/>

<SettingsSharedChangeRoleDialog
<SettingsWorkspacesMembersChangeRoleDialog
v-model:open="showChangeUserRoleDialog"
:workspace-domain-policy-compliant="
userToModify?.user.workspaceDomainPolicyCompliant
"
:current-role="Roles.Workspace.Guest"
:workspace="workspace"
@update-role="onUpdateRole"
/>
</div>
Expand Down Expand Up @@ -137,6 +138,7 @@ graphql(`
id
...SettingsWorkspacesMembersTableHeader_Workspace
...SettingsSharedDeleteUserDialog_Workspace
...SettingsWorkspacesMembersChangeRoleDialog_Workspace
team {
items {
id
Expand Down Expand Up @@ -202,7 +204,7 @@ const actionItems = computed(() => {
]
if (isWorkspaceAdmin.value) {
items.unshift([{ title: 'Update role...', id: ActionTypes.ChangeRole }])
items.unshift([{ title: 'Change role...', id: ActionTypes.ChangeRole }])
}
if (guests.value.find((guest) => guest.projectRoles.length)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@
<div v-else />
</template>
</LayoutTable>
<SettingsSharedChangeRoleDialog
<SettingsWorkspacesMembersChangeRoleDialog
v-model:open="showChangeUserRoleDialog"
:workspace-domain-policy-compliant="userToModify?.workspaceDomainPolicyCompliant"
:current-role="currentUserRole"
:workspace="workspace"
@update-role="onUpdateRole"
/>
<SettingsSharedDeleteUserDialog
Expand Down Expand Up @@ -139,6 +140,7 @@ graphql(`
name
...SettingsSharedDeleteUserDialog_Workspace
...SettingsWorkspacesMembersTableHeader_Workspace
...SettingsWorkspacesMembersChangeRoleDialog_Workspace
team {
items {
id
Expand Down Expand Up @@ -227,7 +229,7 @@ const filteredActionsItems = (user: UserItem) => {
// Allow role change if the active user is an admin
if (isWorkspaceAdmin.value && !isActiveUserCurrentUser.value(user)) {
baseItems.push([{ title: 'Update role...', id: ActionTypes.ChangeRole }])
baseItems.push([{ title: 'Change role...', id: ActionTypes.ChangeRole }])
}
// Allow the current user to leave the workspace
Expand Down
Loading

0 comments on commit 047f21a

Please sign in to comment.