Merge branch 'main' into andrew/web-2920-fe
This commit is contained in:
@@ -1,21 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="!hasValidPlan">
|
||||
<div
|
||||
v-if="condensed"
|
||||
class="flex items-center justify-between rounded-md p-2 pl-3 text-body-3xs font-medium bg-info-lighter text-primary-focus dark:text-foreground gap-x-2"
|
||||
>
|
||||
{{ title }}
|
||||
<FormButton
|
||||
v-if="actions.length > 0"
|
||||
size="sm"
|
||||
:disabled="actions[0].disabled"
|
||||
@click="actions[0].onClick"
|
||||
>
|
||||
{{ actions[0].title }}
|
||||
</FormButton>
|
||||
</div>
|
||||
<CommonAlert v-else :color="alertColor" :actions="actions">
|
||||
<template v-if="showBillingAlert">
|
||||
<CommonAlert :color="alertColor" :actions="actions">
|
||||
<template #title>
|
||||
{{ title }}
|
||||
</template>
|
||||
@@ -28,7 +14,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import {
|
||||
type BillingAlert_WorkspaceFragment,
|
||||
@@ -61,43 +46,18 @@ const props = defineProps<{
|
||||
const { billingPortalRedirect } = useBillingActions()
|
||||
|
||||
const planStatus = computed(() => props.workspace.plan?.status)
|
||||
// If there is no plan status, we assume it's a trial
|
||||
const isTrial = computed(
|
||||
() => !planStatus.value || planStatus.value === WorkspacePlanStatuses.Trial
|
||||
)
|
||||
const isPaymentFailed = computed(
|
||||
() => planStatus.value === WorkspacePlanStatuses.PaymentFailed
|
||||
)
|
||||
const isScheduledForCancelation = computed(
|
||||
() => planStatus.value === WorkspacePlanStatuses.CancelationScheduled
|
||||
)
|
||||
const trialDaysLeft = computed(() => {
|
||||
const createdAt = props.workspace.plan?.createdAt
|
||||
const trialEndDate = dayjs(createdAt).add(31, 'days')
|
||||
const diffDays = trialEndDate.diff(dayjs(), 'day')
|
||||
return Math.max(0, diffDays)
|
||||
})
|
||||
const title = computed(() => {
|
||||
if (isTrial.value) {
|
||||
if (trialDaysLeft.value === 0) {
|
||||
return 'Final day of free trial'
|
||||
}
|
||||
if (props.condensed) {
|
||||
return `${trialDaysLeft.value} day${
|
||||
trialDaysLeft.value !== 1 ? 's' : ''
|
||||
} left in trial`
|
||||
} else
|
||||
return `You have ${trialDaysLeft.value} day${
|
||||
trialDaysLeft.value !== 1 ? 's' : ''
|
||||
} left on your free trial`
|
||||
}
|
||||
switch (planStatus.value) {
|
||||
case WorkspacePlanStatuses.CancelationScheduled:
|
||||
return `Your workspace subscription is scheduled for cancellation`
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return `Your workspace subscription has been cancelled`
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
return `Your free trial has ended`
|
||||
case WorkspacePlanStatuses.PaymentFailed:
|
||||
return "Your last payment didn't go through"
|
||||
default:
|
||||
@@ -105,25 +65,23 @@ const title = computed(() => {
|
||||
}
|
||||
})
|
||||
const description = computed(() => {
|
||||
if (isTrial.value) {
|
||||
return trialDaysLeft.value === 0
|
||||
? 'Upgrade to a paid plan to continue using your workspace'
|
||||
: 'Upgrade to a paid plan to start your subscription'
|
||||
}
|
||||
switch (planStatus.value) {
|
||||
case WorkspacePlanStatuses.CancelationScheduled:
|
||||
return 'Once the current billing cycle ends your workspace will enter read-only mode. Renew your subscription to undo.'
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return 'Your workspace has been cancelled and is in read-only mode. Subscribe to a plan to regain full access.'
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
return "The workspace is in a read-only locked state until there's an active subscription. Subscribe to a plan to regain full access."
|
||||
case WorkspacePlanStatuses.PaymentFailed:
|
||||
return "Update your payment information now to ensure your workspace doesn't go into maintenance mode."
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const hasValidPlan = computed(() => planStatus.value === WorkspacePlanStatuses.Valid)
|
||||
const showBillingAlert = computed(
|
||||
() =>
|
||||
planStatus.value === WorkspacePlanStatuses.PaymentFailed ||
|
||||
planStatus.value === WorkspacePlanStatuses.Canceled ||
|
||||
planStatus.value === WorkspacePlanStatuses.CancelationScheduled
|
||||
)
|
||||
|
||||
const alertColor = computed<AlertColor>(() => {
|
||||
switch (planStatus.value) {
|
||||
@@ -131,7 +89,6 @@ const alertColor = computed<AlertColor>(() => {
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return 'danger'
|
||||
case WorkspacePlanStatuses.CancelationScheduled:
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
return 'warning'
|
||||
default:
|
||||
return 'neutral'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,43 +2,48 @@
|
||||
<div
|
||||
class="border border-outline-3 bg-foundation text-foreground rounded-lg p-5 flex flex-col w-full"
|
||||
>
|
||||
<div class="lg:h-32">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<h4 class="text-body font-medium">
|
||||
{{ formatName(plan) }}
|
||||
</h4>
|
||||
<CommonBadge v-if="badgeText" rounded>
|
||||
{{ badgeText }}
|
||||
</CommonBadge>
|
||||
</div>
|
||||
<p class="text-body mt-1">
|
||||
<span class="font-medium">
|
||||
{{ planPrice }}
|
||||
</span>
|
||||
per seat/month
|
||||
</p>
|
||||
<p
|
||||
v-if="plan === WorkspacePlans.Free"
|
||||
class="text-body-xs text-foreground-2 mt-2.5"
|
||||
>
|
||||
For individuals and small teams trying Speckle.
|
||||
</p>
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-x-2 mt-3 px-1">
|
||||
<FormSwitch
|
||||
v-model="isYearlyIntervalSelected"
|
||||
:show-label="false"
|
||||
name="billing-interval"
|
||||
@update:model-value="
|
||||
(newValue) => $emit('onYearlyIntervalSelected', newValue)
|
||||
"
|
||||
/>
|
||||
<span class="text-body-2xs">Billed yearly</span>
|
||||
<CommonBadge rounded color-classes="text-foreground-2 bg-primary-muted">
|
||||
-10%
|
||||
<div class="lg:h-32 flex flex-col">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<h4 class="text-body font-medium">
|
||||
{{ formatName(plan) }}
|
||||
</h4>
|
||||
<CommonBadge v-if="badgeText" rounded>
|
||||
{{ badgeText }}
|
||||
</CommonBadge>
|
||||
</div>
|
||||
<div class="w-full mt-4">
|
||||
<p class="text-body mt-1">
|
||||
<span class="font-medium">
|
||||
{{ planPrice }}
|
||||
</span>
|
||||
per seat/month
|
||||
</p>
|
||||
<template v-if="plan !== WorkspacePlans.Free">
|
||||
<div class="flex items-center gap-x-2 mt-3 px-1">
|
||||
<FormSwitch
|
||||
v-model="isYearlyIntervalSelected"
|
||||
:show-label="false"
|
||||
name="billing-interval"
|
||||
@update:model-value="
|
||||
(newValue) => $emit('onYearlyIntervalSelected', newValue)
|
||||
"
|
||||
/>
|
||||
<span class="text-body-2xs">Billed yearly</span>
|
||||
<CommonBadge rounded color-classes="text-foreground-2 bg-primary-muted">
|
||||
-10%
|
||||
</CommonBadge>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-full mt-4">
|
||||
<div v-if="hasCta">
|
||||
<slot name="cta" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:key="`tooltip-${yearlyIntervalSelected}-${plan}-${currentPlan?.name}`"
|
||||
v-tippy="buttonTooltip"
|
||||
>
|
||||
<FormButton
|
||||
:color="buttonColor"
|
||||
:disabled="!isSelectable"
|
||||
@@ -48,7 +53,7 @@
|
||||
{{ buttonText }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-y-2 mt-4 pt-3 border-t border-outline-3">
|
||||
<li
|
||||
@@ -95,6 +100,7 @@ import { XMarkIcon } from '@heroicons/vue/24/outline'
|
||||
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
|
||||
import { formatPrice, formatName } from '~/lib/billing/helpers/plan'
|
||||
import { useBillingActions } from '~/lib/billing/composables/actions'
|
||||
import type { SetupContext } from 'vue'
|
||||
|
||||
defineEmits<{
|
||||
(e: 'onYearlyIntervalSelected', value: boolean): void
|
||||
@@ -103,13 +109,14 @@ defineEmits<{
|
||||
const props = defineProps<{
|
||||
plan: WorkspacePlans
|
||||
yearlyIntervalSelected: boolean
|
||||
currentPlan: MaybeNullOrUndefined<WorkspacePlan>
|
||||
isAdmin: boolean
|
||||
activeBillingInterval: MaybeNullOrUndefined<BillingInterval>
|
||||
hasSubscription: boolean
|
||||
workspaceId: MaybeNullOrUndefined<string>
|
||||
canUpgrade: boolean
|
||||
workspaceId?: MaybeNullOrUndefined<string>
|
||||
currentPlan?: MaybeNullOrUndefined<WorkspacePlan>
|
||||
activeBillingInterval?: MaybeNullOrUndefined<BillingInterval>
|
||||
hasSubscription?: MaybeNullOrUndefined<boolean>
|
||||
}>()
|
||||
|
||||
const slots: SetupContext['slots'] = useSlots()
|
||||
const { pricesNew } = useWorkspacePlanPrices()
|
||||
const { upgradePlan, redirectToCheckout } = useBillingActions()
|
||||
|
||||
@@ -130,6 +137,8 @@ const planPrice = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const hasCta = computed(() => !!slots.cta)
|
||||
|
||||
const canUpgradeToPlan = computed(() => {
|
||||
if (!props.currentPlan) return false
|
||||
|
||||
@@ -151,9 +160,12 @@ const isDowngrade = computed(() => {
|
||||
return !canUpgradeToPlan.value && props.currentPlan?.name !== props.plan
|
||||
})
|
||||
|
||||
const isCurrentPlan = computed(
|
||||
() => isMatchingInterval.value && props.currentPlan?.name === props.plan
|
||||
)
|
||||
const isCurrentPlan = computed(() => {
|
||||
if (props.plan === WorkspacePlans.Free) {
|
||||
return props.currentPlan?.name === props.plan
|
||||
}
|
||||
return isMatchingInterval.value && props.currentPlan?.name === props.plan
|
||||
})
|
||||
|
||||
const isAnnualToMonthly = computed(() => {
|
||||
return (
|
||||
@@ -172,7 +184,10 @@ const isMonthlyToAnnual = computed(() => {
|
||||
})
|
||||
|
||||
const isSelectable = computed(() => {
|
||||
if (!props.isAdmin) return false
|
||||
if (!props.canUpgrade) return false
|
||||
// Free CTA has no clickable scenario
|
||||
if (props.plan === WorkspacePlans.Free) return false
|
||||
|
||||
// Always enable buttons during expired or canceled state
|
||||
if (
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Expired ||
|
||||
@@ -210,6 +225,10 @@ const buttonColor = computed(() => {
|
||||
})
|
||||
|
||||
const buttonText = computed(() => {
|
||||
// Current plan case
|
||||
if (isCurrentPlan.value) {
|
||||
return 'Current plan'
|
||||
}
|
||||
// Allow if current plan is Free, or the current plan is expired/canceled
|
||||
if (
|
||||
props.currentPlan?.name === WorkspacePlans.Free ||
|
||||
@@ -218,10 +237,6 @@ const buttonText = computed(() => {
|
||||
) {
|
||||
return `Subscribe to ${formatName(props.plan)}`
|
||||
}
|
||||
// Current plan case
|
||||
if (isCurrentPlan.value) {
|
||||
return 'Current plan'
|
||||
}
|
||||
// Billing interval and lower plan case
|
||||
if (isDowngrade.value) {
|
||||
return `Downgrade to ${props.plan}`
|
||||
@@ -237,6 +252,37 @@ const buttonText = computed(() => {
|
||||
return canUpgradeToPlan.value ? `Upgrade to ${formatName(props.plan)}` : ''
|
||||
})
|
||||
|
||||
const buttonTooltip = computed(() => {
|
||||
if (!props.canUpgrade) {
|
||||
return 'You must be a workspace admin.'
|
||||
}
|
||||
|
||||
if (
|
||||
isCurrentPlan.value ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Expired ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Canceled
|
||||
)
|
||||
return undefined
|
||||
|
||||
if (isDowngrade.value) {
|
||||
return 'Downgrading is not supported at the moment. Please contact billing@speckle.systems.'
|
||||
}
|
||||
|
||||
if (isAnnualToMonthly.value) {
|
||||
return 'Changing from an annual to a monthly plan is currently not supported. Please contact billing@speckle.systems.'
|
||||
}
|
||||
|
||||
if (
|
||||
props.activeBillingInterval === BillingInterval.Yearly &&
|
||||
!props.yearlyIntervalSelected &&
|
||||
canUpgradeToPlan.value
|
||||
) {
|
||||
return 'Upgrading from an annual plan to a monthly plan is not supported. Please contact billing@speckle.systems.'
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const badgeText = computed(() =>
|
||||
props.currentPlan?.name === props.plan ? 'Current plan' : ''
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:current-plan="currentPlan"
|
||||
:yearly-interval-selected="isYearlySelected"
|
||||
:active-billing-interval="billingInterval"
|
||||
:is-admin="isAdmin"
|
||||
:can-upgrade="isAdmin"
|
||||
:workspace-id="props.workspaceId"
|
||||
:has-subscription="!!subscription"
|
||||
@on-yearly-interval-selected="onYearlyIntervalSelected"
|
||||
@@ -19,16 +19,8 @@
|
||||
import { BillingInterval } from '~/lib/common/generated/gql/graphql'
|
||||
import { WorkspacePlans } from '@speckle/shared'
|
||||
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { type MaybeNullOrUndefined, type WorkspaceRoles, Roles } from '@speckle/shared'
|
||||
|
||||
graphql(`
|
||||
fragment PricingTable_Workspace on Workspace {
|
||||
id
|
||||
role
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string
|
||||
role: MaybeNullOrUndefined<WorkspaceRoles>
|
||||
|
||||
@@ -69,66 +69,10 @@
|
||||
v-if="showWorkspaceSettings"
|
||||
title="Workspace settings"
|
||||
>
|
||||
<template v-if="isWorkspaceNewPlansEnabled" #title-icon>
|
||||
<template #title-icon>
|
||||
<IconWorkspaces class="size-4" />
|
||||
</template>
|
||||
<template v-if="!isWorkspaceNewPlansEnabled">
|
||||
<LayoutSidebarMenuGroup
|
||||
v-for="workspaceItem in workspaceItems"
|
||||
:key="`workspace-item-${workspaceItem.slug}`"
|
||||
:title="workspaceItem.name"
|
||||
collapsible
|
||||
:collapsed="slug !== workspaceItem.slug"
|
||||
:tag="
|
||||
workspaceItem.plan?.status === WorkspacePlanStatuses.Trial ||
|
||||
!workspaceItem.plan?.status
|
||||
? 'TRIAL'
|
||||
: undefined
|
||||
"
|
||||
nested
|
||||
>
|
||||
<template #title-icon>
|
||||
<WorkspaceAvatar
|
||||
:logo="workspaceItem.logo"
|
||||
:name="workspaceItem.name"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
<NuxtLink
|
||||
v-for="workspaceMenuItem in workspaceMenuItems"
|
||||
:key="`workspace-menu-item-${workspaceMenuItem.name}-${workspaceItem.slug}`"
|
||||
:to="
|
||||
!isAdmin &&
|
||||
(workspaceMenuItem.disabled ||
|
||||
needsSsoSession(workspaceItem, workspaceMenuItem.name))
|
||||
? undefined
|
||||
: workspaceMenuItem.route(workspaceItem.slug)
|
||||
"
|
||||
@click="isOpenMobile = false"
|
||||
>
|
||||
<LayoutSidebarMenuGroupItem
|
||||
v-if="workspaceMenuItem.permission?.includes(workspaceItem.role as WorkspaceRoles)"
|
||||
:label="workspaceMenuItem.title"
|
||||
:active="
|
||||
route.name?.toString().startsWith(workspaceMenuItem.name) &&
|
||||
route.params.slug === workspaceItem.slug
|
||||
"
|
||||
:tooltip-text="
|
||||
needsSsoSession(workspaceItem, workspaceMenuItem.name)
|
||||
? 'Log in with your SSO provider to access this page'
|
||||
: workspaceMenuItem.tooltipText
|
||||
"
|
||||
:disabled="
|
||||
!isAdmin &&
|
||||
(workspaceMenuItem.disabled ||
|
||||
needsSsoSession(workspaceItem, workspaceMenuItem.name))
|
||||
"
|
||||
class="!pl-8"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</LayoutSidebarMenuGroup>
|
||||
</template>
|
||||
<template v-else-if="activeWorkspaceItem">
|
||||
<template v-if="activeWorkspaceItem">
|
||||
<NuxtLink
|
||||
v-for="workspaceMenuItem in workspaceMenuItems"
|
||||
:key="`workspace-menu-item-${workspaceMenuItem.name}-${activeWorkspaceItem}`"
|
||||
@@ -189,10 +133,7 @@ import {
|
||||
settingsWorkspaceRoutes,
|
||||
workspaceRoute
|
||||
} from '~/lib/common/helpers/route'
|
||||
import {
|
||||
WorkspacePlanStatuses,
|
||||
type SettingsMenu_WorkspaceFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import type { SettingsMenu_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { useNavigation } from '~~/lib/navigation/composables/navigation'
|
||||
@@ -226,7 +167,6 @@ graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { activeWorkspaceSlug } = useNavigation()
|
||||
const settingsMenuState = useSettingsMenuState()
|
||||
@@ -241,7 +181,6 @@ const isMobile = breakpoints.smaller('lg')
|
||||
|
||||
const isOpenMobile = ref(false)
|
||||
|
||||
const slug = computed(() => route.params.slug as string)
|
||||
const workspaceItems = computed(
|
||||
() =>
|
||||
workspaceResult.value?.activeUser?.workspaces.items.filter(
|
||||
@@ -252,12 +191,10 @@ const activeWorkspaceItem = computed(() =>
|
||||
workspaceItems.value.find((item) => item.slug === activeWorkspaceSlug.value)
|
||||
)
|
||||
const wrapperClasses = computed(() => {
|
||||
// TODO: Simplify post WS migration
|
||||
const width = isWorkspaceNewPlansEnabled.value ? '13' : '17'
|
||||
return [
|
||||
'absolute z-40 lg:static h-full flex shrink-0 transition-all',
|
||||
`w-[${width}rem]`,
|
||||
isOpenMobile.value ? '' : `-translate-x-[${width}rem] lg:translate-x-0`
|
||||
`w-[13rem]`,
|
||||
isOpenMobile.value ? '' : `-translate-x-[13rem] lg:translate-x-0`
|
||||
]
|
||||
})
|
||||
|
||||
@@ -284,7 +221,6 @@ const exitSettingsRoute = computed(() => {
|
||||
|
||||
const showWorkspaceSettings = computed(() => {
|
||||
if (!isWorkspacesEnabled.value) return false
|
||||
if (isWorkspaceNewPlansEnabled.value) return !!activeWorkspaceSlug.value
|
||||
return true
|
||||
return !!activeWorkspaceSlug.value
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,271 +1,95 @@
|
||||
<!-- "Old" billing page, the component for the new workspace plans is in PageNew.vue -->
|
||||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
|
||||
<FormButton
|
||||
v-if="isWorkspaceNewPlansEnabled && isServerAdmin"
|
||||
size="lg"
|
||||
class="!bg-pink-500 !border-pink-700 mb-4"
|
||||
@click="handleUpgradeClick"
|
||||
>
|
||||
𝓒𝓱𝓪𝓷𝓰𝓮 𝓽𝓸 𝓷𝓮𝔀 𝓹𝓵𝓪𝓷 💸
|
||||
</FormButton>
|
||||
<SettingsSectionHeader title="Billing" text="Your workspace billing details" />
|
||||
<template v-if="isBillingIntegrationEnabled">
|
||||
<div class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<BillingAlert
|
||||
v-if="workspace && workspace?.plan?.status !== WorkspacePlanStatuses.Valid"
|
||||
:workspace="workspace"
|
||||
/>
|
||||
<SettingsSectionHeader title="Billing summary" subheading class="pt-4" />
|
||||
<div class="border border-outline-3 rounded-lg">
|
||||
<div
|
||||
class="grid grid-cols-1 lg:grid-cols-3 divide-y divide-outline-3 lg:divide-y-0 lg:divide-x"
|
||||
>
|
||||
<div class="p-5 pt-4 flex flex-col gap-y-1">
|
||||
<h3 class="text-body-xs text-foreground-2 pb-1">
|
||||
{{ summaryPlanHeading }}
|
||||
</h3>
|
||||
<div class="flex gap-x-2">
|
||||
<p class="text-heading-lg text-foreground">
|
||||
Workspace
|
||||
<span class="capitalize">
|
||||
{{ currentPlan?.name ?? WorkspacePlans.Starter }}
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<CommonBadge v-if="showStatusBadge" rounded>TRIAL</CommonBadge>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="isPurchasablePlan" class="text-body-xs text-foreground-2">
|
||||
<span v-if="statusIsTrial">
|
||||
<span class="line-through mr-1">
|
||||
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
|
||||
seat/month
|
||||
</span>
|
||||
Free
|
||||
</span>
|
||||
<span
|
||||
v-else-if="currentPlan?.status === WorkspacePlanStatuses.Expired"
|
||||
>
|
||||
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
|
||||
seat/month
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
|
||||
seat/month, billed
|
||||
{{
|
||||
subscription?.billingInterval === BillingInterval.Yearly
|
||||
? 'annually'
|
||||
: 'monthly'
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-5 pt-4 flex flex-col gap-y-1">
|
||||
<template v-if="isPurchasablePlan || statusIsTrial">
|
||||
<h3 class="text-body-xs text-foreground-2 pb-1">
|
||||
{{ summaryBillHeading }}
|
||||
</h3>
|
||||
<p class="text-heading-lg text-foreground inline-block">
|
||||
{{ summaryBillValue }} per
|
||||
{{
|
||||
subscription?.billingInterval === BillingInterval.Yearly
|
||||
? 'year'
|
||||
: 'month'
|
||||
}}
|
||||
</p>
|
||||
<p class="text-body-xs text-foreground-2 flex gap-x-1 items-center">
|
||||
{{ summaryBillDescription }}
|
||||
<InformationCircleIcon
|
||||
v-tippy="billTooltip"
|
||||
class="w-4 h-4 text-foreground cursor-pointer"
|
||||
/>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h3 class="text-body-xs text-foreground-2 pb-1">Expected bill</h3>
|
||||
<p class="text-heading-lg text-foreground inline-block">
|
||||
{{ isAcademiaPlan ? 'Free' : 'Not applicable' }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<div class="p-5 pt-4 flex flex-col gap-y-1">
|
||||
<h3 class="text-body-xs text-foreground-2 pb-1">
|
||||
{{ summaryDateHeading }}
|
||||
</h3>
|
||||
<p class="text-heading-lg text-foreground capitalize">
|
||||
{{ isPurchasablePlan ? nextPaymentDue : 'Never' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="showSummaryDateDescription"
|
||||
class="text-body-xs text-foreground-2"
|
||||
>
|
||||
<span v-if="statusIsTrial">Subscribe before this date</span>
|
||||
<span v-else>
|
||||
{{
|
||||
subscription?.billingInterval === BillingInterval.Yearly
|
||||
? 'Annual'
|
||||
: 'Monthly'
|
||||
}}
|
||||
billing period
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActivePlan && isPurchasablePlan"
|
||||
class="flex flex-row gap-x-4 p-5 items-center border-t border-outline-3"
|
||||
>
|
||||
<div class="text-body-xs gap-y-2 flex-1">
|
||||
<p class="font-medium text-foreground">Billing portal</p>
|
||||
<p class="text-foreground-2">
|
||||
View invoices, edit payment details, and manage your subscription.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormButton color="outline" @click="billingPortalRedirect(workspace?.id)">
|
||||
Open billing portal
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isPurchasablePlan || statusIsTrial">
|
||||
<SettingsSectionHeader
|
||||
:title="pricingTableHeading"
|
||||
subheading
|
||||
class="pt-4"
|
||||
/>
|
||||
<SettingsWorkspacesBillingPricingTable
|
||||
:workspace-id="workspace?.id"
|
||||
:current-plan="currentPlan"
|
||||
:active-billing-interval="subscription?.billingInterval"
|
||||
:is-admin="isAdmin"
|
||||
@on-plan-selected="onPlanSelected"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="isInvoicedPlan" class="mt-8 text-foreground-2 text-body-xs">
|
||||
Need help?
|
||||
<a
|
||||
class="text-foreground hover:underline"
|
||||
href="mailto:billing@speckle.systems"
|
||||
@click="
|
||||
mixpanel.track('Workspace Support Link Clicked', {
|
||||
workspace_id: workspace?.id,
|
||||
plan: currentPlan?.name
|
||||
})
|
||||
"
|
||||
>
|
||||
Contact us
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isPurchasablePlan"
|
||||
class="mt-8 text-center text-foreground-2 text-body-xs"
|
||||
>
|
||||
Need help?
|
||||
<NuxtLink
|
||||
class="text-foreground"
|
||||
:to="guideBillingUrl"
|
||||
target="_blank"
|
||||
@click="
|
||||
mixpanel.track('Workspace Docs Link Clicked', {
|
||||
workspace_id: workspace?.id,
|
||||
plan: currentPlan?.name
|
||||
})
|
||||
"
|
||||
>
|
||||
<span class="hover:underline">Read the docs</span>
|
||||
</NuxtLink>
|
||||
or
|
||||
<a
|
||||
class="text-foreground hover:underline"
|
||||
href="mailto:billing@speckle.systems"
|
||||
@click="
|
||||
mixpanel.track('Workspace Support Link Clicked', {
|
||||
workspace_id: workspace?.id,
|
||||
plan: currentPlan?.name
|
||||
})
|
||||
"
|
||||
>
|
||||
contact support
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<SettingsWorkspacesBillingUpgradeDialog
|
||||
v-if="selectedPlanName && selectedPlanCycle && workspace?.id"
|
||||
v-model:open="isUpgradeDialogOpen"
|
||||
:plan="selectedPlanName"
|
||||
:billing-interval="selectedPlanCycle"
|
||||
:workspace-id="workspace.id"
|
||||
/>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0 flex flex-col gap-y-2 md:gap-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Billing and plans"
|
||||
text="Update your payment information or switch plans according to your needs"
|
||||
/>
|
||||
<CommonAlert v-if="!isNewPlan" color="danger">
|
||||
<template #title>You are on an old plan</template>
|
||||
<template #description>
|
||||
<p>If you are a server admin use the buttons below to upgrade</p>
|
||||
</template>
|
||||
</CommonAlert>
|
||||
<div class="flex flex-col gap-y-6 md:gap-y-10">
|
||||
<section v-if="isServerAdmin" class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<div class="flex gap-x-4">
|
||||
<FormButton
|
||||
size="lg"
|
||||
class="!bg-pink-500 !border-pink-700 mb-4"
|
||||
@click="handleUpgradeClick(WorkspacePlans.Free)"
|
||||
>
|
||||
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 free 𝖕𝖑𝖆𝖓
|
||||
</FormButton>
|
||||
<FormButton
|
||||
size="lg"
|
||||
class="!bg-pink-500 !border-pink-700 mb-4"
|
||||
@click="handleUpgradeClick(WorkspacePlans.Team)"
|
||||
>
|
||||
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 Starter 𝖕𝖑𝖆𝖓
|
||||
</FormButton>
|
||||
<FormButton
|
||||
size="lg"
|
||||
class="!bg-pink-500 !border-pink-700 mb-4"
|
||||
@click="handleUpgradeClick(WorkspacePlans.Pro)"
|
||||
>
|
||||
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 Business 𝖕𝖑𝖆𝖓
|
||||
</FormButton>
|
||||
</div>
|
||||
</section>
|
||||
<template v-if="isNewPlan">
|
||||
<section v-if="isPurchasablePlan" class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Summary" subheading />
|
||||
<SettingsWorkspacesBillingSummary :workspace-id="workspace?.id" />
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Usage" subheading />
|
||||
<SettingsWorkspacesBillingUsage :slug="slug" />
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Upgrade your plan" subheading />
|
||||
<PricingTable
|
||||
:slug="slug"
|
||||
:workspace-id="workspace?.id"
|
||||
:role="workspace?.role as WorkspaceRoles"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Add-ons" subheading />
|
||||
<SettingsWorkspacesBillingAddOns :slug="slug" />
|
||||
</section>
|
||||
</template>
|
||||
<template v-else>Coming soon</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
<script lang="ts" setup>
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import { adminUpdateWorkspacePlanMutation } from '~/lib/billing/graphql/mutations'
|
||||
import { settingsWorkspaceBillingQuery } from '~/lib/settings/graphql/queries'
|
||||
import { useIsBillingIntegrationEnabled } from '~/composables/globals'
|
||||
import {
|
||||
WorkspacePlans,
|
||||
WorkspacePlanStatuses,
|
||||
BillingInterval,
|
||||
WorkspacePaymentMethod,
|
||||
type PaidWorkspacePlans
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { useBillingActions } from '~/lib/billing/composables/actions'
|
||||
import type { PaidWorkspacePlansOld } from '@speckle/shared'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { InformationCircleIcon } from '@heroicons/vue/24/outline'
|
||||
import { isPaidPlan } from '@/lib/billing/helpers/types'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { guideBillingUrl } from '~/lib/common/helpers/route'
|
||||
import { adminUpdateWorkspacePlanMutation } from '~/lib/billing/graphql/mutations'
|
||||
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
|
||||
import { formatPrice } from '~/lib/billing/helpers/plan'
|
||||
PaidWorkspacePlanStatuses,
|
||||
type WorkspaceRoles
|
||||
} from '@speckle/shared'
|
||||
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesBilling_Workspace on Workspace {
|
||||
...BillingAlert_Workspace
|
||||
fragment WorkspaceBillingPage_Workspace on Workspace {
|
||||
id
|
||||
role
|
||||
plan {
|
||||
name
|
||||
status
|
||||
createdAt
|
||||
paymentMethod
|
||||
}
|
||||
subscription {
|
||||
billingInterval
|
||||
currentBillingCycleEnd
|
||||
seats {
|
||||
guest
|
||||
plan
|
||||
}
|
||||
}
|
||||
team {
|
||||
items {
|
||||
id
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const slug = computed(() => (route.params.slug as string) || '')
|
||||
|
||||
const { prices } = useWorkspacePlanPrices()
|
||||
const { isAdmin: isServerAdmin } = useActiveUser()
|
||||
const route = useRoute()
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
const slug = computed(() => (route.params.slug as string) || '')
|
||||
const { isAdmin: isServerAdmin } = useActiveUser()
|
||||
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
|
||||
const { isPurchasablePlan, isNewPlan } = useWorkspacePlan(slug.value)
|
||||
const { mutate: mutateWorkspacePlan } = useMutation(adminUpdateWorkspacePlanMutation)
|
||||
const { result: workspaceResult } = useQuery(
|
||||
settingsWorkspaceBillingQuery,
|
||||
() => ({
|
||||
@@ -275,203 +99,21 @@ const { result: workspaceResult } = useQuery(
|
||||
enabled: isBillingIntegrationEnabled
|
||||
})
|
||||
)
|
||||
const { billingPortalRedirect, redirectToCheckout } = useBillingActions()
|
||||
const mixpanel = useMixpanel()
|
||||
const { mutate: mutateWorkspacePlan } = useMutation(adminUpdateWorkspacePlanMutation)
|
||||
|
||||
const selectedPlanName = ref<PaidWorkspacePlansOld>()
|
||||
const selectedPlanCycle = ref<BillingInterval>()
|
||||
const isUpgradeDialogOpen = ref(false)
|
||||
|
||||
const seatPrices = computed(() => ({
|
||||
[WorkspacePlans.Starter]: prices.value?.[WorkspacePlans.Starter],
|
||||
[WorkspacePlans.Plus]: prices.value?.[WorkspacePlans.Plus],
|
||||
[WorkspacePlans.Business]: prices.value?.[WorkspacePlans.Business],
|
||||
...(isWorkspaceNewPlansEnabled.value
|
||||
? {
|
||||
[WorkspacePlans.Team]: prices.value?.[WorkspacePlans.Team],
|
||||
[WorkspacePlans.Pro]: prices.value?.[WorkspacePlans.Pro]
|
||||
}
|
||||
: {})
|
||||
}))
|
||||
const workspace = computed(() => workspaceResult.value?.workspaceBySlug)
|
||||
const currentPlan = computed(() => workspace.value?.plan)
|
||||
const subscription = computed(() => workspace.value?.subscription)
|
||||
const statusIsTrial = computed(
|
||||
() =>
|
||||
currentPlan.value?.status === WorkspacePlanStatuses.Trial ||
|
||||
!currentPlan.value?.status
|
||||
)
|
||||
const isActivePlan = computed(
|
||||
() =>
|
||||
currentPlan.value &&
|
||||
currentPlan.value?.status !== WorkspacePlanStatuses.Trial &&
|
||||
currentPlan.value?.status !== WorkspacePlanStatuses.Canceled &&
|
||||
currentPlan.value?.status !== WorkspacePlanStatuses.Expired
|
||||
)
|
||||
|
||||
const isAcademiaPlan = computed(
|
||||
() => currentPlan.value?.name === WorkspacePlans.Academia
|
||||
)
|
||||
const isPurchasablePlan = computed(() => isPaidPlan(currentPlan.value?.name))
|
||||
const seatPrice = computed(() =>
|
||||
currentPlan.value && subscription.value
|
||||
? seatPrices.value?.[currentPlan.value.name as keyof typeof seatPrices.value]?.[
|
||||
subscription.value.billingInterval
|
||||
]
|
||||
: seatPrices.value?.[WorkspacePlans.Starter]?.[BillingInterval.Monthly]
|
||||
)
|
||||
const nextPaymentDue = computed(() =>
|
||||
isPurchasablePlan.value
|
||||
? subscription.value?.currentBillingCycleEnd
|
||||
? dayjs(subscription.value?.currentBillingCycleEnd).format('MMMM D, YYYY')
|
||||
: dayjs(currentPlan.value?.createdAt).add(31, 'days').format('MMMM D, YYYY')
|
||||
: 'Never'
|
||||
)
|
||||
const isInvoicedPlan = computed(
|
||||
() => currentPlan.value?.paymentMethod === WorkspacePaymentMethod.Invoice
|
||||
)
|
||||
const isAdmin = computed(() => workspace.value?.role === Roles.Workspace.Admin)
|
||||
const guestSeatCount = computed(() =>
|
||||
isActivePlan.value
|
||||
? workspace.value?.subscription?.seats.guest ?? 0
|
||||
: workspace.value?.team.items.filter((user) => user.role === Roles.Workspace.Guest)
|
||||
.length ?? 0
|
||||
)
|
||||
const memberSeatCount = computed(() =>
|
||||
isActivePlan.value
|
||||
? workspace.value?.subscription?.seats.plan ?? 0
|
||||
: workspace.value
|
||||
? workspace.value.team.items.length - guestSeatCount.value
|
||||
: 0
|
||||
)
|
||||
const summaryBillValue = computed(() => {
|
||||
if (!seatPrice.value) return 'loading'
|
||||
const guestPrice =
|
||||
seatPrice.value[Roles.Workspace.Guest].amount * guestSeatCount.value
|
||||
const memberPrice =
|
||||
seatPrice.value[Roles.Workspace.Member].amount * memberSeatCount.value
|
||||
const totalPrice = guestPrice + memberPrice
|
||||
const isAnnual = subscription.value?.billingInterval === BillingInterval.Yearly
|
||||
return isPurchasablePlan.value ? `£${isAnnual ? totalPrice * 12 : totalPrice}` : '£0'
|
||||
})
|
||||
const summaryBillDescription = computed(() => {
|
||||
const memberText =
|
||||
memberSeatCount.value > 1 ? `${memberSeatCount.value} members` : '1 member'
|
||||
const guestText =
|
||||
guestSeatCount.value > 1 ? `${guestSeatCount.value} guests` : '1 guest'
|
||||
|
||||
return `${memberText}${guestSeatCount.value > 0 ? `, ${guestText}` : ''}`
|
||||
})
|
||||
const billTooltip = computed(() => {
|
||||
if (!seatPrice.value) return undefined
|
||||
|
||||
const memberText = `${memberSeatCount.value} member${
|
||||
memberSeatCount.value === 1 ? '' : 's'
|
||||
} at ${formatPrice(seatPrice.value[Roles.Workspace.Member])}/month`
|
||||
const guestText = `${guestSeatCount.value} guest${
|
||||
guestSeatCount.value === 1 ? '' : 's'
|
||||
} at ${formatPrice(seatPrice.value[Roles.Workspace.Guest])}/month`
|
||||
|
||||
return `${memberText}${guestSeatCount.value > 0 ? `, ${guestText}` : ''}`
|
||||
})
|
||||
const summaryPlanHeading = computed(() => {
|
||||
switch (currentPlan.value?.status) {
|
||||
case WorkspacePlanStatuses.Trial:
|
||||
return 'Trial plan'
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return 'Plan'
|
||||
default:
|
||||
return 'Current plan'
|
||||
}
|
||||
})
|
||||
const summaryBillHeading = computed(() => {
|
||||
switch (currentPlan.value?.status) {
|
||||
case WorkspacePlanStatuses.Trial:
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return 'Expected bill'
|
||||
default:
|
||||
return subscription.value?.billingInterval === BillingInterval.Yearly
|
||||
? 'Annual bill'
|
||||
: 'Monthly bill'
|
||||
}
|
||||
})
|
||||
const summaryDateHeading = computed(() => {
|
||||
if (statusIsTrial.value && isPurchasablePlan.value) {
|
||||
return 'Trial ends'
|
||||
} else if (currentPlan.value?.status === WorkspacePlanStatuses.Expired) {
|
||||
return 'Trial expired at'
|
||||
} else if (currentPlan.value?.status === WorkspacePlanStatuses.Canceled) {
|
||||
return 'Cancels'
|
||||
} else {
|
||||
return 'Next payment due'
|
||||
}
|
||||
})
|
||||
const showSummaryDateDescription = computed(() => {
|
||||
return statusIsTrial.value && isPurchasablePlan.value
|
||||
})
|
||||
|
||||
const pricingTableHeading = computed(() => {
|
||||
switch (currentPlan.value?.status) {
|
||||
case WorkspacePlanStatuses.Trial:
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
return 'Start your subscription'
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return 'Restart your subscription'
|
||||
default:
|
||||
return 'Upgrade your plan'
|
||||
}
|
||||
})
|
||||
const showStatusBadge = computed(() => {
|
||||
return (
|
||||
(statusIsTrial.value ||
|
||||
currentPlan.value?.status === WorkspacePlanStatuses.Expired) &&
|
||||
isPurchasablePlan.value
|
||||
)
|
||||
})
|
||||
|
||||
const onPlanSelected = (plan: {
|
||||
name: PaidWorkspacePlansOld
|
||||
cycle: BillingInterval
|
||||
}) => {
|
||||
const { name, cycle } = plan
|
||||
if (!isPaidPlan(name) || !workspace.value?.id) return
|
||||
|
||||
if (
|
||||
statusIsTrial.value ||
|
||||
currentPlan.value?.status === WorkspacePlanStatuses.Expired ||
|
||||
currentPlan.value?.status === WorkspacePlanStatuses.Canceled
|
||||
) {
|
||||
mixpanel.track('Workspace Subscribe Button Clicked', {
|
||||
plan,
|
||||
cycle,
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspace.value?.id
|
||||
})
|
||||
|
||||
redirectToCheckout({
|
||||
plan: name as unknown as PaidWorkspacePlans,
|
||||
cycle,
|
||||
workspaceId: workspace.value?.id
|
||||
})
|
||||
} else {
|
||||
selectedPlanName.value = name
|
||||
selectedPlanCycle.value = cycle
|
||||
isUpgradeDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpgradeClick = () => {
|
||||
if (!workspace.value?.id) return
|
||||
// Temporary hack to change workspace plans to the new free plan
|
||||
// Temporary hack to change workspace plans to the new free plan
|
||||
const handleUpgradeClick = (plan: WorkspacePlans) => {
|
||||
if (!workspaceResult.value?.workspaceBySlug.id) return
|
||||
mutateWorkspacePlan({
|
||||
input: {
|
||||
workspaceId: workspace.value?.id,
|
||||
plan: WorkspacePlans.Free,
|
||||
status: WorkspacePlanStatuses.Valid
|
||||
workspaceId: workspaceResult.value.workspaceBySlug.id,
|
||||
plan,
|
||||
status: PaidWorkspacePlanStatuses.Valid
|
||||
}
|
||||
})
|
||||
|
||||
// Reload to show the new plan, will be gone soon
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
<!-- This is a temporary component and will replace SettingsWorkspacesBillingPage post-migration -->
|
||||
<template>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0 flex flex-col gap-y-2 md:gap-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Billing and plans"
|
||||
text="Update your payment information or switch plans according to your needs"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-y-6 md:gap-y-10">
|
||||
<!-- Temporary until we can test with real upgrades -->
|
||||
<section v-if="isServerAdmin" class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<div class="flex gap-x-4">
|
||||
<FormButton
|
||||
size="lg"
|
||||
class="!bg-pink-500 !border-pink-700 mb-4"
|
||||
@click="handleUpgradeClick(WorkspacePlans.Free)"
|
||||
>
|
||||
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 𝖋𝖗𝖊𝖊 𝖕𝖑𝖆𝖓
|
||||
</FormButton>
|
||||
<FormButton
|
||||
size="lg"
|
||||
class="!bg-pink-500 !border-pink-700 mb-4"
|
||||
@click="handleUpgradeClick(WorkspacePlans.Team)"
|
||||
>
|
||||
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 𝖙𝖊𝖆𝖒 𝖕𝖑𝖆𝖓
|
||||
</FormButton>
|
||||
<FormButton
|
||||
size="lg"
|
||||
class="!bg-pink-500 !border-pink-700 mb-4"
|
||||
@click="handleUpgradeClick(WorkspacePlans.Pro)"
|
||||
>
|
||||
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 𝖕𝖗𝖔 𝖕𝖑𝖆𝖓
|
||||
</FormButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="isPurchasablePlan" class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Summary" subheading />
|
||||
<SettingsWorkspacesBillingSummary :workspace-id="workspace?.id" />
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Usage" subheading />
|
||||
<SettingsWorkspacesBillingUsage />
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Upgrade your plan" subheading />
|
||||
<PricingTable
|
||||
:slug="slug"
|
||||
:workspace-id="workspace?.id"
|
||||
:role="workspace?.role as WorkspaceRoles"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-y-4 md:gap-y-6">
|
||||
<SettingsSectionHeader title="Add-ons" subheading />
|
||||
<SettingsWorkspacesBillingAddOns :slug="slug" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import { adminUpdateWorkspacePlanMutation } from '~/lib/billing/graphql/mutations'
|
||||
import { settingsWorkspaceBillingQueryNew } from '~/lib/settings/graphql/queries'
|
||||
import {
|
||||
WorkspacePlans,
|
||||
PaidWorkspacePlanStatuses,
|
||||
type WorkspaceRoles
|
||||
} from '@speckle/shared'
|
||||
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceBillingPageNew_Workspace on Workspace {
|
||||
id
|
||||
...PricingTable_Workspace
|
||||
}
|
||||
`)
|
||||
|
||||
const route = useRoute()
|
||||
const slug = computed(() => (route.params.slug as string) || '')
|
||||
const { isAdmin: isServerAdmin } = useActiveUser()
|
||||
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
|
||||
const { isPurchasablePlan } = useWorkspacePlan(slug.value)
|
||||
const { mutate: mutateWorkspacePlan } = useMutation(adminUpdateWorkspacePlanMutation)
|
||||
const { result: workspaceResult } = useQuery(
|
||||
settingsWorkspaceBillingQueryNew,
|
||||
() => ({
|
||||
slug: slug.value
|
||||
}),
|
||||
() => ({
|
||||
enabled: isBillingIntegrationEnabled
|
||||
})
|
||||
)
|
||||
|
||||
const workspace = computed(() => workspaceResult.value?.workspaceBySlug)
|
||||
|
||||
// Temporary hack to change workspace plans to the new free plan
|
||||
const handleUpgradeClick = (plan: WorkspacePlans) => {
|
||||
if (!workspaceResult.value?.workspaceBySlug.id) return
|
||||
mutateWorkspacePlan({
|
||||
input: {
|
||||
workspaceId: workspaceResult.value.workspaceBySlug.id,
|
||||
plan,
|
||||
status: PaidWorkspacePlanStatuses.Valid
|
||||
}
|
||||
})
|
||||
|
||||
// Reload to show the new plan, will be gone soon
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
@@ -1,318 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="border border-outline-3 bg-foundation text-foreground rounded-lg p-5 flex flex-col w-full"
|
||||
>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<h4 class="text-body font-medium">
|
||||
Workspace
|
||||
<span class="capitalize">{{ plan }}</span>
|
||||
</h4>
|
||||
<CommonBadge v-if="badgeText" rounded>
|
||||
{{ badgeText }}
|
||||
</CommonBadge>
|
||||
</div>
|
||||
<p class="text-body mt-1">
|
||||
<span class="font-medium">
|
||||
{{
|
||||
formatPrice(
|
||||
props.yearlyIntervalSelected && planPrice?.['workspace:member']
|
||||
? {
|
||||
...planPrice['workspace:member'],
|
||||
amount: planPrice['workspace:member'].amount * 0.8
|
||||
}
|
||||
: planPrice?.['workspace:member']
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
per seat/month
|
||||
</p>
|
||||
<div class="flex items-center gap-x-2 mt-3 px-1">
|
||||
<FormSwitch
|
||||
v-model="isYearlyIntervalSelected"
|
||||
:show-label="false"
|
||||
name="domain-protection"
|
||||
@update:model-value="(newValue) => $emit('onYearlyIntervalSelected', newValue)"
|
||||
/>
|
||||
<span class="text-body-2xs">Billed annually</span>
|
||||
<CommonBadge rounded color-classes="text-foreground-2 bg-primary-muted">
|
||||
20% off
|
||||
</CommonBadge>
|
||||
</div>
|
||||
<div v-if="workspaceId || hasCta" class="w-full mt-4">
|
||||
<div v-if="hasCta">
|
||||
<slot name="cta" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- Key to fix tippy reactivity -->
|
||||
<div
|
||||
:key="`tooltip-${yearlyIntervalSelected}-${plan}-${currentPlan?.name}`"
|
||||
v-tippy="buttonTooltip"
|
||||
>
|
||||
<FormButton
|
||||
:color="buttonColor"
|
||||
:disabled="!isSelectable"
|
||||
full-width
|
||||
@click="onCtaClick"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-y-2 mt-4 pt-3 border-t border-outline-3">
|
||||
<li
|
||||
v-for="(featureMetadata, feature) in WorkspacePlanFeaturesMetadata"
|
||||
:key="feature"
|
||||
class="flex items-center text-body-xs"
|
||||
:class="{
|
||||
'lg:hidden': !planFeatures.includes(feature)
|
||||
}"
|
||||
>
|
||||
<IconCheck
|
||||
v-if="planFeatures.includes(feature)"
|
||||
class="w-4 h-4 text-foreground mx-2"
|
||||
/>
|
||||
<XMarkIcon v-else class="w-4 h-4 mx-2 text-foreground-3" />
|
||||
<span
|
||||
v-tippy="
|
||||
isFunction(featureMetadata.description)
|
||||
? featureMetadata.description({
|
||||
price: formatPrice(planPrice?.[Roles.Workspace.Guest])
|
||||
})
|
||||
: featureMetadata.description
|
||||
"
|
||||
class="underline decoration-outline-5 decoration-dashed underline-offset-4 cursor-help"
|
||||
:class="{
|
||||
'text-foreground-2': !planFeatures.includes(feature)
|
||||
}"
|
||||
>
|
||||
{{ featureMetadata.displayName }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<SettingsWorkspacesBillingUpgradeDialog
|
||||
v-if="currentPlan?.name && workspaceId"
|
||||
v-model:open="isUpgradeDialogOpen"
|
||||
:plan="plan"
|
||||
:billing-interval="
|
||||
yearlyIntervalSelected ? BillingInterval.Yearly : BillingInterval.Monthly
|
||||
"
|
||||
:workspace-id="workspaceId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type PaidWorkspacePlansOld,
|
||||
type MaybeNullOrUndefined,
|
||||
WorkspacePlans,
|
||||
WorkspacePlanFeaturesMetadata
|
||||
} from '@speckle/shared'
|
||||
import { Roles, WorkspacePlanConfigs } from '@speckle/shared'
|
||||
import {
|
||||
type WorkspacePlan,
|
||||
WorkspacePlanStatuses,
|
||||
BillingInterval
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { startCase, isFunction } from 'lodash'
|
||||
import { XMarkIcon } from '@heroicons/vue/24/outline'
|
||||
import type { SetupContext } from 'vue'
|
||||
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
|
||||
import { formatPrice } from '~/lib/billing/helpers/plan'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'onYearlyIntervalSelected', value: boolean): void
|
||||
(e: 'onPlanSelected', value: PaidWorkspacePlansOld): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
plan: PaidWorkspacePlansOld
|
||||
yearlyIntervalSelected: boolean
|
||||
badgeText?: string
|
||||
// The following props are optional if the table is for informational purposes
|
||||
currentPlan?: MaybeNullOrUndefined<WorkspacePlan>
|
||||
workspaceId?: string
|
||||
isAdmin?: boolean
|
||||
activeBillingInterval?: BillingInterval
|
||||
}>()
|
||||
|
||||
const slots: SetupContext['slots'] = useSlots()
|
||||
const { prices } = useWorkspacePlanPrices()
|
||||
|
||||
const isUpgradeDialogOpen = ref(false)
|
||||
const isYearlyIntervalSelected = ref(props.yearlyIntervalSelected)
|
||||
|
||||
const planFeatures = computed(() => WorkspacePlanConfigs[props.plan].features)
|
||||
const planPrice = computed(() => prices.value?.[props.plan]?.monthly)
|
||||
|
||||
const hasCta = computed(() => !!slots.cta)
|
||||
const canUpgradeToPlan = computed(() => {
|
||||
if (!props.currentPlan) return false
|
||||
|
||||
const allowedUpgrades: Record<WorkspacePlans, WorkspacePlans[]> = {
|
||||
[WorkspacePlans.Starter]: [WorkspacePlans.Plus, WorkspacePlans.Business],
|
||||
[WorkspacePlans.Plus]: [WorkspacePlans.Business],
|
||||
[WorkspacePlans.Business]: [],
|
||||
[WorkspacePlans.Academia]: [],
|
||||
[WorkspacePlans.Unlimited]: [],
|
||||
[WorkspacePlans.StarterInvoiced]: [],
|
||||
[WorkspacePlans.PlusInvoiced]: [],
|
||||
[WorkspacePlans.BusinessInvoiced]: [],
|
||||
// New
|
||||
[WorkspacePlans.Free]: [],
|
||||
[WorkspacePlans.Team]: [],
|
||||
[WorkspacePlans.Pro]: []
|
||||
}
|
||||
|
||||
return allowedUpgrades[props.currentPlan.name].includes(props.plan)
|
||||
})
|
||||
|
||||
const statusIsTrial = computed(
|
||||
() => props.currentPlan?.status === WorkspacePlanStatuses.Trial || !props.currentPlan
|
||||
)
|
||||
|
||||
const isMatchingInterval = computed(
|
||||
() =>
|
||||
props.activeBillingInterval ===
|
||||
(props.yearlyIntervalSelected ? BillingInterval.Yearly : BillingInterval.Monthly)
|
||||
)
|
||||
|
||||
const isDowngrade = computed(() => {
|
||||
return !canUpgradeToPlan.value && props.currentPlan?.name !== props.plan
|
||||
})
|
||||
|
||||
const isCurrentPlan = computed(
|
||||
() => isMatchingInterval.value && props.currentPlan?.name === props.plan
|
||||
)
|
||||
|
||||
const isAnnualToMonthly = computed(() => {
|
||||
return (
|
||||
!isMatchingInterval.value &&
|
||||
props.currentPlan?.name === props.plan &&
|
||||
!props.yearlyIntervalSelected
|
||||
)
|
||||
})
|
||||
|
||||
const isMonthlyToAnnual = computed(() => {
|
||||
return (
|
||||
!isMatchingInterval.value &&
|
||||
props.currentPlan?.name === props.plan &&
|
||||
props.yearlyIntervalSelected
|
||||
)
|
||||
})
|
||||
|
||||
const isSelectable = computed(() => {
|
||||
if (!props.isAdmin) return false
|
||||
// Always enable buttons during trial, expired or canceled state
|
||||
if (
|
||||
statusIsTrial.value ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Expired ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Canceled
|
||||
)
|
||||
return true
|
||||
|
||||
// Allow selection if switching from monthly to yearly for the same plan
|
||||
if (isMonthlyToAnnual.value && props.currentPlan?.name === props.plan) return true
|
||||
|
||||
// Disable if current plan and intervals match
|
||||
if (isCurrentPlan.value) return false
|
||||
|
||||
// Handle billing interval changes
|
||||
if (!isMatchingInterval.value) {
|
||||
// Allow yearly upgrades from monthly plans
|
||||
if (isMonthlyToAnnual.value) return canUpgradeToPlan.value
|
||||
|
||||
// Never allow switching to monthly if currently on yearly billing
|
||||
if (props.activeBillingInterval === BillingInterval.Yearly) return false
|
||||
|
||||
// Allow monthly plan changes only for upgrades
|
||||
return canUpgradeToPlan.value
|
||||
}
|
||||
|
||||
// Allow upgrades to higher tier plans
|
||||
return canUpgradeToPlan.value
|
||||
})
|
||||
|
||||
const buttonColor = computed(() => {
|
||||
if (
|
||||
statusIsTrial.value ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Expired
|
||||
) {
|
||||
return props.plan === WorkspacePlans.Starter ? 'primary' : 'outline'
|
||||
}
|
||||
return 'outline'
|
||||
})
|
||||
|
||||
const buttonText = computed(() => {
|
||||
// Allow selection during trial, expired or canceled state
|
||||
if (
|
||||
statusIsTrial.value ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Expired ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Canceled
|
||||
) {
|
||||
return `Subscribe to ${startCase(props.plan)}`
|
||||
}
|
||||
// Current plan case
|
||||
if (isCurrentPlan.value) {
|
||||
return 'Current plan'
|
||||
}
|
||||
// Billing interval and lower plan case
|
||||
if (isDowngrade.value) {
|
||||
return `Downgrade to ${props.plan}`
|
||||
}
|
||||
// Billing interval change and current plan
|
||||
if (isAnnualToMonthly.value) {
|
||||
return 'Change to monthly plan'
|
||||
}
|
||||
if (isMonthlyToAnnual.value) {
|
||||
return 'Change to annual plan'
|
||||
}
|
||||
// Upgrade case
|
||||
return canUpgradeToPlan.value ? `Upgrade to ${startCase(props.plan)}` : ''
|
||||
})
|
||||
|
||||
const buttonTooltip = computed(() => {
|
||||
if (!props.isAdmin) {
|
||||
return 'You must be a workspace admin.'
|
||||
}
|
||||
|
||||
if (
|
||||
statusIsTrial.value ||
|
||||
isCurrentPlan.value ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Expired ||
|
||||
props.currentPlan?.status === WorkspacePlanStatuses.Canceled
|
||||
)
|
||||
return undefined
|
||||
|
||||
if (isDowngrade.value) {
|
||||
return 'Downgrading is not supported at the moment. Please contact billing@speckle.systems.'
|
||||
}
|
||||
|
||||
if (isAnnualToMonthly.value) {
|
||||
return 'Changing from an annual to a monthly plan is currently not supported. Please contact billing@speckle.systems.'
|
||||
}
|
||||
|
||||
if (
|
||||
props.activeBillingInterval === BillingInterval.Yearly &&
|
||||
!props.yearlyIntervalSelected &&
|
||||
canUpgradeToPlan.value
|
||||
) {
|
||||
return 'Upgrading from an annual plan to a monthly plan is not supported. Please contact billing@speckle.systems.'
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const onCtaClick = () => {
|
||||
emit('onPlanSelected', props.plan)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.yearlyIntervalSelected,
|
||||
(newValue) => {
|
||||
isYearlyIntervalSelected.value = newValue
|
||||
}
|
||||
)
|
||||
</script>
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col lg:grid lg:grid-cols-3 gap-4 w-full">
|
||||
<SettingsWorkspacesBillingPricingTablePlan
|
||||
v-for="plan in oldPlans"
|
||||
:key="plan"
|
||||
:plan="plan"
|
||||
:yearly-interval-selected="isYearlySelected"
|
||||
v-bind="$props"
|
||||
@on-yearly-interval-selected="onYearlyIntervalSelected"
|
||||
@on-plan-selected="onPlanSelected"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// DEPCRECATED: This is only used for old workspace plans
|
||||
import { type WorkspacePlan, BillingInterval } from '~/lib/common/generated/gql/graphql'
|
||||
import { type MaybeNullOrUndefined, PaidWorkspacePlansOld } from '@speckle/shared'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'onPlanSelected',
|
||||
value: { name: PaidWorkspacePlansOld; cycle: BillingInterval }
|
||||
): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
currentPlan?: MaybeNullOrUndefined<WorkspacePlan>
|
||||
workspaceId?: string
|
||||
isAdmin?: boolean
|
||||
activeBillingInterval?: BillingInterval
|
||||
}>()
|
||||
|
||||
const isYearlySelected = ref(false)
|
||||
const oldPlans = computed(() => Object.values(PaidWorkspacePlansOld))
|
||||
|
||||
const onYearlyIntervalSelected = (newValue: boolean) => {
|
||||
isYearlySelected.value = newValue
|
||||
}
|
||||
|
||||
const onPlanSelected = (value: PaidWorkspacePlansOld) => {
|
||||
emit('onPlanSelected', {
|
||||
name: value,
|
||||
cycle: isYearlySelected.value ? BillingInterval.Yearly : BillingInterval.Monthly
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.activeBillingInterval,
|
||||
(newVal) => {
|
||||
isYearlySelected.value = newVal === BillingInterval.Yearly
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
<!-- TODO: Update these with real limits once available -->
|
||||
<template>
|
||||
<div class="border border-outline-3 rounded-lg divide-y divide-outline-3">
|
||||
<div class="px-5 py-8 gap-y-6 flex flex-col sm:items-center sm:flex-row">
|
||||
<div
|
||||
class="flex-1 flex flex-col gap-y-4 xl:w-[66%] lg:grid lg:grid-cols-2 lg:gap-x-4"
|
||||
>
|
||||
<div>
|
||||
<h3 class="text-body-xs text-foreground-2 pb-2">Editor seats</h3>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-body-xs text-foreground font-medium leading-none">4</p>
|
||||
<CommonBadge rounded>2 unused</CommonBadge>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-body-xs text-foreground-2 pb-2">Viewer seats</h3>
|
||||
<p class="text-body-xs text-foreground font-medium leading-none">4</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex xl:w-[34%] xl:justify-end">
|
||||
<FormButton
|
||||
color="outline"
|
||||
@click="navigateTo(settingsWorkspaceRoutes.members.route(slug))"
|
||||
>
|
||||
Manage members
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 py-8 gap-y-6 flex flex-col sm:items-center sm:flex-row">
|
||||
<div
|
||||
class="flex-1 flex flex-col gap-y-4 xl:w-[66%] lg:grid lg:grid-cols-2 lg:gap-x-4"
|
||||
>
|
||||
<div>
|
||||
<h3 class="text-body-xs text-foreground-2 pb-2">Projects</h3>
|
||||
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
|
||||
{{ formatUsageText(10, 100, 'project') }}
|
||||
</p>
|
||||
<CommonProgressBar
|
||||
class="max-w-72 w-full"
|
||||
:current-value="10"
|
||||
:max-value="100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-body-xs text-foreground-2 pb-2">Models</h3>
|
||||
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
|
||||
{{ formatUsageText(10, 100, 'model') }}
|
||||
</p>
|
||||
<CommonProgressBar
|
||||
class="max-w-72 w-full"
|
||||
:current-value="10"
|
||||
:max-value="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex xl:w-[34%] xl:justify-end">
|
||||
<FormButton
|
||||
color="outline"
|
||||
@click="navigateTo(settingsWorkspaceRoutes.projects.route(slug))"
|
||||
>
|
||||
Manage projects
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { settingsWorkspaceRoutes } from '~/lib/common/helpers/route'
|
||||
|
||||
defineProps<{
|
||||
slug: string
|
||||
}>()
|
||||
|
||||
const formatUsageText = (current: number, max: number, type: string) => {
|
||||
return `${current} ${type}${current === 1 ? '' : 's'} used / ${max} included`
|
||||
}
|
||||
</script>
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<div class="border border-outline-3 rounded-lg p-6 flex items-center justify-between">
|
||||
<div class="flex-1 flex flex-col">
|
||||
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
|
||||
{{ text }}
|
||||
</p>
|
||||
<CommonProgressBar
|
||||
class="max-w-72 w-full"
|
||||
:current-value="currentValue"
|
||||
:max-value="maxValue"
|
||||
/>
|
||||
</div>
|
||||
<FormButton color="outline">{{ buttonText }}</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CommonProgressBar } from '@speckle/ui-components'
|
||||
|
||||
type UsageType = 'seat' | 'project' | 'model'
|
||||
|
||||
const props = defineProps<{
|
||||
buttonText: string
|
||||
currentValue: number
|
||||
maxValue: number
|
||||
type: UsageType
|
||||
}>()
|
||||
|
||||
const text = computed(() => {
|
||||
return `${props.currentValue} ${props.type}${
|
||||
props.currentValue === 1 ? '' : 's'
|
||||
} used / ${props.maxValue} included`
|
||||
})
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
<!-- TODO: Update these with real limits once available -->
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<SettingsWorkspacesBillingUsageCard
|
||||
button-text="Manage seats"
|
||||
:current-value="7"
|
||||
:max-value="8"
|
||||
type="seat"
|
||||
/>
|
||||
<SettingsWorkspacesBillingUsageCard
|
||||
button-text="Manage projects"
|
||||
:current-value="7"
|
||||
:max-value="50"
|
||||
type="project"
|
||||
/>
|
||||
<SettingsWorkspacesBillingUsageCard
|
||||
button-text="Manage models"
|
||||
:current-value="50"
|
||||
:max-value="50"
|
||||
type="model"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
@@ -1,302 +0,0 @@
|
||||
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
||||
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
|
||||
<template>
|
||||
<div class="group h-full">
|
||||
<template v-if="isLoggedIn">
|
||||
<Portal to="mobile-navigation">
|
||||
<div class="lg:hidden">
|
||||
<FormButton
|
||||
:color="isOpenMobile ? 'outline' : 'subtle'"
|
||||
size="sm"
|
||||
class="mt-px"
|
||||
@click="isOpenMobile = !isOpenMobile"
|
||||
>
|
||||
<IconSidebar v-if="!isOpenMobile" class="h-4 w-4 -ml-1 -mr-1" />
|
||||
<IconSidebarClose v-else class="h-4 w-4 -ml-1 -mr-1" />
|
||||
</FormButton>
|
||||
</div>
|
||||
</Portal>
|
||||
<div
|
||||
v-keyboard-clickable
|
||||
class="lg:hidden absolute inset-0 backdrop-blur-sm z-40 transition-all"
|
||||
:class="isOpenMobile ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
@click="isOpenMobile = false"
|
||||
/>
|
||||
<div
|
||||
class="absolute z-40 lg:static h-full flex w-[17rem] shrink-0 transition-all"
|
||||
:class="isOpenMobile ? '' : '-translate-x-[17rem] lg:translate-x-0'"
|
||||
>
|
||||
<LayoutSidebar
|
||||
class="border-r border-outline-3 px-2 pt-3 pb-2 bg-foundation-page"
|
||||
>
|
||||
<LayoutSidebarMenu>
|
||||
<LayoutSidebarMenuGroup>
|
||||
<NuxtLink :to="homeRoute" @click="isOpenMobile = false">
|
||||
<LayoutSidebarMenuGroupItem
|
||||
label="Dashboard"
|
||||
:active="isActive(homeRoute)"
|
||||
>
|
||||
<template #icon>
|
||||
<HomeIcon class="size-4 stroke-[1.5px]" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink :to="projectsRoute" @click="isOpenMobile = false">
|
||||
<LayoutSidebarMenuGroupItem
|
||||
label="Projects"
|
||||
:active="isActive(projectsRoute)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconProjects class="size-4 text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink :to="connectorsRoute" @click="isOpenMobile = false">
|
||||
<LayoutSidebarMenuGroupItem
|
||||
label="Connectors"
|
||||
:active="isActive(connectorsRoute)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconConnectors class="size-4 ml-px text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink :to="tutorialsRoute" @click="isOpenMobile = false">
|
||||
<LayoutSidebarMenuGroupItem
|
||||
label="Tutorials"
|
||||
:active="isActive(tutorialsRoute)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconTutorials class="size-4 ml-px text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
</LayoutSidebarMenuGroup>
|
||||
|
||||
<LayoutSidebarMenuGroup
|
||||
v-if="isWorkspacesEnabled"
|
||||
collapsible
|
||||
title="Workspaces"
|
||||
:icon-click="isNotGuest ? handlePlusClick : undefined"
|
||||
icon-text="Create workspace"
|
||||
>
|
||||
<NuxtLink :to="workspacesRoute" @click="handleIntroducingWorkspacesClick">
|
||||
<LayoutSidebarMenuGroupItem
|
||||
v-if="!hasWorkspaces || route.path === workspacesRoute"
|
||||
label="Introducing workspaces"
|
||||
:active="isActive(workspacesRoute)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconWorkspaces class="size-4 text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
<template v-for="(item, key) in workspacesItems" :key="key">
|
||||
<NuxtLink
|
||||
v-if="item.creationState.completed !== false"
|
||||
:to="item.to"
|
||||
@click="isOpenMobile = false"
|
||||
>
|
||||
<LayoutSidebarMenuGroupItem
|
||||
:label="item.label"
|
||||
:active="isActive(item.to)"
|
||||
:tag="
|
||||
item.plan.status === WorkspacePlanStatuses.Trial ||
|
||||
item.plan.status === WorkspacePlanStatuses.Expired ||
|
||||
!item.plan.status
|
||||
? 'TRIAL'
|
||||
: undefined
|
||||
"
|
||||
class="!pl-1"
|
||||
>
|
||||
<template #icon>
|
||||
<WorkspaceAvatar :name="item.name" :logo="item.logo" size="sm" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</LayoutSidebarMenuGroup>
|
||||
|
||||
<LayoutSidebarMenuGroup title="Resources" collapsible>
|
||||
<NuxtLink
|
||||
to="https://speckle.community/"
|
||||
target="_blank"
|
||||
@click="isOpenMobile = false"
|
||||
>
|
||||
<LayoutSidebarMenuGroupItem label="Community forum" external>
|
||||
<template #icon>
|
||||
<IconCommunity class="size-4 text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
|
||||
<div @click="openFeedbackDialog">
|
||||
<LayoutSidebarMenuGroupItem label="Give us feedback">
|
||||
<template #icon>
|
||||
<IconFeedback class="size-4 text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</div>
|
||||
|
||||
<NuxtLink
|
||||
to="https://speckle.guide/"
|
||||
target="_blank"
|
||||
@click="isOpenMobile = false"
|
||||
>
|
||||
<LayoutSidebarMenuGroupItem label="Documentation" external>
|
||||
<template #icon>
|
||||
<IconDocumentation class="size-4 text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
to="https://speckle.community/c/making-speckle/changelog"
|
||||
target="_blank"
|
||||
@click="isOpenMobile = false"
|
||||
>
|
||||
<LayoutSidebarMenuGroupItem label="Changelog" external>
|
||||
<template #icon>
|
||||
<IconChangelog class="size-4 text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
</LayoutSidebarMenuGroup>
|
||||
</LayoutSidebarMenu>
|
||||
</LayoutSidebar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<FeedbackDialog v-model:open="showFeedbackDialog" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
FormButton,
|
||||
LayoutSidebar,
|
||||
LayoutSidebarMenu,
|
||||
LayoutSidebarMenuGroup,
|
||||
LayoutSidebarMenuGroupItem
|
||||
} from '@speckle/ui-components'
|
||||
import { settingsSidebarQuery } from '~/lib/settings/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import {
|
||||
homeRoute,
|
||||
projectsRoute,
|
||||
workspaceRoute,
|
||||
workspacesRoute,
|
||||
workspaceCreateRoute,
|
||||
connectorsRoute,
|
||||
tutorialsRoute
|
||||
} from '~/lib/common/helpers/route'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { HomeIcon } from '@heroicons/vue/24/outline'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { WorkspacePlanStatuses } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment Sidebar_User on User {
|
||||
id
|
||||
automateFunctions {
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { activeUser: user } = useActiveUser()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const isOpenMobile = ref(false)
|
||||
const showFeedbackDialog = ref(false)
|
||||
|
||||
const { result: workspaceResult, onResult: onWorkspaceResult } = useQuery(
|
||||
settingsSidebarQuery,
|
||||
null,
|
||||
{
|
||||
enabled: isWorkspacesEnabled.value
|
||||
}
|
||||
)
|
||||
|
||||
const isActive = (...routes: string[]): boolean => {
|
||||
return routes.some((routeTo) => route.path === routeTo)
|
||||
}
|
||||
|
||||
const isNotGuest = computed(
|
||||
() => Roles.Server.Admin || user.value?.role === Roles.Server.User
|
||||
)
|
||||
const workspacesItems = computed(() =>
|
||||
workspaceResult.value?.activeUser
|
||||
? workspaceResult.value.activeUser.workspaces.items.map((workspace) => ({
|
||||
label: workspace.name,
|
||||
name: workspace.name,
|
||||
id: workspace.id,
|
||||
to: workspaceRoute(workspace.slug),
|
||||
logo: workspace.logo,
|
||||
plan: {
|
||||
status: workspace.plan?.status
|
||||
},
|
||||
creationState: {
|
||||
completed: workspace.creationState?.completed
|
||||
}
|
||||
}))
|
||||
: []
|
||||
)
|
||||
const hasWorkspaces = computed(() => workspacesItems.value.length > 0)
|
||||
|
||||
onWorkspaceResult((result) => {
|
||||
if (result.data?.activeUser) {
|
||||
const workspaceIds = result.data.activeUser.workspaces.items.map(
|
||||
(workspace) => workspace.id
|
||||
)
|
||||
|
||||
if (workspaceIds.length > 0) {
|
||||
mixpanel.people.set('workspace_id', workspaceIds)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const openFeedbackDialog = () => {
|
||||
showFeedbackDialog.value = true
|
||||
isOpenMobile.value = false
|
||||
}
|
||||
|
||||
const openWorkspaceWizard = () => {
|
||||
navigateTo(workspaceCreateRoute())
|
||||
mixpanel.track('Create Workspace Button Clicked', {
|
||||
source: 'sidebar'
|
||||
})
|
||||
}
|
||||
|
||||
const handlePlusClick = () => {
|
||||
if (route.path === workspacesRoute) {
|
||||
openWorkspaceWizard()
|
||||
} else {
|
||||
mixpanel.track('Clicked Link to Workspace Explainer', {
|
||||
source: 'sidebar'
|
||||
})
|
||||
router.push(workspacesRoute)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIntroducingWorkspacesClick = () => {
|
||||
isOpenMobile.value = false
|
||||
mixpanel.track('Clicked Link to Workspace Explainer', {
|
||||
source: 'sidebar'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<SidebarNew v-if="isWorkspaceNewPlansEnabled" />
|
||||
<Sidebar v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Temporary wrapper to hold both the old and new sidebars
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
</script>
|
||||
@@ -1,15 +1,8 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 lg:gap-4">
|
||||
<div v-if="!isWorkspaceGuest && !isInTrial && !hasValidPlan">
|
||||
<div v-if="!isWorkspaceGuest">
|
||||
<BillingAlert :workspace="workspaceInfo" :actions="billingAlertAction" />
|
||||
</div>
|
||||
<div v-if="!isWorkspaceGuest && isInTrial" class="lg:hidden">
|
||||
<BillingAlert
|
||||
:workspace="workspaceInfo"
|
||||
:actions="billingAlertAction"
|
||||
condensed
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3 lg:gap-4">
|
||||
<template v-if="isWorkspaceNewPlansEnabled">
|
||||
@@ -121,20 +114,12 @@ const { activeUser } = useActiveUser()
|
||||
const isWorkspaceAdmin = computed(
|
||||
() => props.workspaceInfo.role === Roles.Workspace.Admin
|
||||
)
|
||||
const isInTrial = computed(
|
||||
() =>
|
||||
props.workspaceInfo.plan?.status === WorkspacePlanStatuses.Trial ||
|
||||
!props.workspaceInfo.plan
|
||||
)
|
||||
const hasValidPlan = computed(
|
||||
() => props.workspaceInfo.plan?.status === WorkspacePlanStatuses.Valid
|
||||
)
|
||||
const isWorkspaceGuest = computed(
|
||||
() => props.workspaceInfo.role === Roles.Workspace.Guest
|
||||
)
|
||||
const billingAlertAction = computed<Array<AlertAction>>(() => {
|
||||
if (
|
||||
(isInTrial.value && isWorkspaceAdmin.value) ||
|
||||
isWorkspaceAdmin.value ||
|
||||
props.workspaceInfo.plan?.status === WorkspacePlanStatuses.Expired
|
||||
) {
|
||||
return [
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
<div class="w-full">
|
||||
<LayoutSidebar>
|
||||
<div class="flex flex-col divide-y divide-outline-3">
|
||||
<div v-if="!isWorkspaceGuest && isInTrial" class="p-4">
|
||||
<BillingAlert
|
||||
:workspace="workspaceInfo"
|
||||
:actions="billingAlertAction"
|
||||
condensed
|
||||
/>
|
||||
</div>
|
||||
<div class="px-4 py-2">
|
||||
<WorkspaceSidebarAbout
|
||||
:workspace-info="workspaceInfo"
|
||||
@@ -35,13 +28,9 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { LayoutSidebar, type AlertAction } from '@speckle/ui-components'
|
||||
import {
|
||||
WorkspacePlanStatuses,
|
||||
type WorkspaceProjectList_WorkspaceFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { LayoutSidebar } from '@speckle/ui-components'
|
||||
import type { WorkspaceProjectList_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { settingsWorkspaceRoutes } from '~/lib/common/helpers/route'
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceSidebar_Workspace on Workspace {
|
||||
@@ -67,29 +56,8 @@ const props = defineProps<{
|
||||
const isWorkspaceGuest = computed(
|
||||
() => props.workspaceInfo.role === Roles.Workspace.Guest
|
||||
)
|
||||
|
||||
const isWorkspaceAdmin = computed(
|
||||
() => props.workspaceInfo.role === Roles.Workspace.Admin
|
||||
)
|
||||
|
||||
const isInTrial = computed(
|
||||
() =>
|
||||
props.workspaceInfo.plan?.status === WorkspacePlanStatuses.Trial ||
|
||||
!props.workspaceInfo.plan
|
||||
)
|
||||
|
||||
const hasDomains = computed(() => props.workspaceInfo.domains?.length)
|
||||
|
||||
const billingAlertAction = computed<Array<AlertAction>>(() => {
|
||||
if (isInTrial.value && isWorkspaceAdmin.value) {
|
||||
return [
|
||||
{
|
||||
title: 'Subscribe',
|
||||
onClick: () =>
|
||||
navigateTo(settingsWorkspaceRoutes.billing.route(props.workspaceInfo.slug))
|
||||
}
|
||||
]
|
||||
}
|
||||
return []
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,32 +2,28 @@
|
||||
<WorkspaceWizardStep title="Choose a plan">
|
||||
<div class="flex flex-col max-w-5xl w-full items-center">
|
||||
<div class="grid lg:grid-cols-3 gap-y-2 gap-x-2 w-full">
|
||||
<SettingsWorkspacesBillingPricingTablePlan
|
||||
v-for="plan in oldPlans"
|
||||
<PricingTablePlan
|
||||
v-for="plan in plans"
|
||||
:key="plan"
|
||||
:plan="plan"
|
||||
:yearly-interval-selected="isYearlySelected"
|
||||
:badge-text="
|
||||
plan === WorkspacePlans.Starter && !isYearlySelected
|
||||
? '30-day free trial'
|
||||
: undefined
|
||||
"
|
||||
can-upgrade
|
||||
@on-yearly-interval-selected="onYearlyIntervalSelected"
|
||||
>
|
||||
<template #cta>
|
||||
<FormButton
|
||||
:color="plan === WorkspacePlans.Starter ? 'primary' : 'outline'"
|
||||
:color="plan === WorkspacePlans.Free ? 'primary' : 'outline'"
|
||||
full-width
|
||||
@click="onCtaClick(plan)"
|
||||
>
|
||||
{{
|
||||
plan === WorkspacePlans.Starter && !isYearlySelected
|
||||
? 'Start 30-day free trial'
|
||||
plan === WorkspacePlans.Free && !isYearlySelected
|
||||
? 'Get started for free'
|
||||
: `Subscribe to ${startCase(plan)}`
|
||||
}}
|
||||
</FormButton>
|
||||
</template>
|
||||
</SettingsWorkspacesBillingPricingTablePlan>
|
||||
</PricingTablePlan>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 mt-4 w-full md:max-w-96">
|
||||
<FormButton color="subtle" size="lg" full-width @click.stop="goToPreviousStep">
|
||||
@@ -39,22 +35,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type PaidWorkspacePlans,
|
||||
BillingInterval,
|
||||
WorkspacePlans
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { BillingInterval } from '~/lib/common/generated/gql/graphql'
|
||||
import { useWorkspacesWizard } from '~/lib/workspaces/composables/wizard'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { WorkspacePlans, type PaidWorkspacePlans } from '@speckle/shared'
|
||||
import { startCase } from 'lodash'
|
||||
import { PaidWorkspacePlansOld } from '@speckle/shared'
|
||||
|
||||
const { goToNextStep, goToPreviousStep, state } = useWorkspacesWizard()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const isYearlySelected = ref(false)
|
||||
|
||||
const oldPlans = computed(() => Object.values(PaidWorkspacePlansOld))
|
||||
const plans = computed(() => [
|
||||
WorkspacePlans.Free,
|
||||
WorkspacePlans.Team,
|
||||
WorkspacePlans.Pro
|
||||
])
|
||||
|
||||
const onCtaClick = (plan: WorkspacePlans) => {
|
||||
state.value.plan = plan as unknown as PaidWorkspacePlans
|
||||
@@ -76,8 +72,8 @@ const onYearlyIntervalSelected = (newValue: boolean) => {
|
||||
|
||||
watch(
|
||||
() => state.value.billingInterval,
|
||||
() => {
|
||||
isYearlySelected.value = state.value.billingInterval === BillingInterval.Yearly
|
||||
(newVal) => {
|
||||
isYearlySelected.value = newVal === BillingInterval.Yearly
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="h-12 w-full shrink-0"></div>
|
||||
|
||||
<div class="relative flex h-[calc(100dvh-3rem)]">
|
||||
<SidebarWrapper />
|
||||
<DashboardSidebar />
|
||||
|
||||
<main class="w-full h-full overflow-y-auto simple-scrollbar pt-4 lg:pt-6 pb-16">
|
||||
<div class="container mx-auto px-6 md:px-8">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="h-12 w-full shrink-0"></div>
|
||||
|
||||
<div class="relative flex h-[calc(100dvh-3rem)]">
|
||||
<SidebarWrapper />
|
||||
<DashboardSidebar />
|
||||
|
||||
<main class="w-full h-full overflow-y-auto simple-scrollbar pt-4 lg:pt-6 pb-16">
|
||||
<div class="container mx-auto px-6 md:px-8">
|
||||
|
||||
@@ -53,11 +53,10 @@ type Documents = {
|
||||
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": typeof types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": typeof types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\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 PricingTable_Workspace on Workspace {\n id\n role\n }\n": typeof types.PricingTable_WorkspaceFragmentDoc,
|
||||
"\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,
|
||||
"\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.ProjectModelPageVersionsPaginationFragmentDoc,
|
||||
"\n fragment ProjectModelPageVersionsProject on Project {\n ...ProjectPageProjectHeader\n model(id: $modelId) {\n id\n name\n pendingImportedVersions {\n ...PendingFileUpload\n }\n }\n ...ProjectModelPageVersionsPagination\n ...ProjectsModelPageEmbed_Project\n workspace {\n id\n readOnly\n }\n }\n": typeof types.ProjectModelPageVersionsProjectFragmentDoc,
|
||||
@@ -121,8 +120,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {\n id\n name\n }\n": typeof types.SettingsWorkspaceGeneralDeleteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n }\n": typeof types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n": typeof types.SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": typeof types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceBillingPageNew_Workspace on Workspace {\n id\n ...PricingTable_Workspace\n }\n": typeof types.WorkspaceBillingPageNew_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceBillingPage_Workspace on Workspace {\n id\n role\n }\n": typeof types.WorkspaceBillingPage_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersChangeRoleDialog_Workspace on Workspace {\n id\n plan {\n status\n name\n }\n subscription {\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersChangeRoleDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
|
||||
@@ -314,8 +312,7 @@ type Documents = {
|
||||
"\n query SettingsSidebar {\n activeUser {\n ...SettingsSidebar_User\n }\n }\n": typeof types.SettingsSidebarDocument,
|
||||
"\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n": typeof types.SettingsSidebarAutomateFunctionsDocument,
|
||||
"\n query SettingsWorkspaceGeneral($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": typeof types.SettingsWorkspaceGeneralDocument,
|
||||
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": typeof types.SettingsWorkspaceBillingDocument,
|
||||
"\n query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPageNew_Workspace\n }\n }\n": typeof types.SettingsWorkspaceBillingNewDocument,
|
||||
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPage_Workspace\n }\n }\n": typeof types.SettingsWorkspaceBillingDocument,
|
||||
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": typeof types.SettingsWorkspaceBillingCustomerPortalDocument,
|
||||
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": typeof types.SettingsWorkspaceRegionsDocument,
|
||||
"\n query SettingsWorkspacesMembers($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n }\n }\n": typeof types.SettingsWorkspacesMembersDocument,
|
||||
@@ -357,7 +354,7 @@ type Documents = {
|
||||
"\n fragment DiscoverableList_Discoverable on User {\n discoverableWorkspaces {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n": typeof types.DiscoverableList_DiscoverableFragmentDoc,
|
||||
"\n fragment DiscoverableList_Requests on User {\n workspaceJoinRequests {\n items {\n id\n status\n workspace {\n id\n name\n logo\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n }\n }\n": typeof types.DiscoverableList_RequestsFragmentDoc,
|
||||
"\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n": typeof types.UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\n }\n }\n }\n": typeof types.WorkspacesPlan_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n usage {\n projectCount\n modelCount\n }\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\n }\n }\n }\n": typeof types.WorkspacesPlan_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspacePlanLimits_Workspace on Workspace {\n id\n projects(limit: 0) {\n totalCount\n items {\n id\n models(limit: 0) {\n totalCount\n }\n }\n }\n plan {\n name\n }\n }\n": typeof types.WorkspacePlanLimits_WorkspaceFragmentDoc,
|
||||
"\n subscription OnWorkspaceProjectsUpdate($slug: String!) {\n workspaceProjectsUpdated(workspaceId: null, workspaceSlug: $slug) {\n projectId\n workspaceId\n type\n project {\n ...ProjectDashboardItem\n }\n }\n }\n ": typeof types.OnWorkspaceProjectsUpdateDocument,
|
||||
"\n fragment WorkspaceHasCustomDataResidency_Workspace on Workspace {\n id\n defaultRegion {\n id\n name\n }\n }\n": typeof types.WorkspaceHasCustomDataResidency_WorkspaceFragmentDoc,
|
||||
@@ -462,11 +459,10 @@ const documents: Documents = {
|
||||
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\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 PricingTable_Workspace on Workspace {\n id\n role\n }\n": types.PricingTable_WorkspaceFragmentDoc,
|
||||
"\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,
|
||||
"\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n ...ProjectsModelPageEmbed_Project\n }\n": types.ProjectModelPageVersionsPaginationFragmentDoc,
|
||||
"\n fragment ProjectModelPageVersionsProject on Project {\n ...ProjectPageProjectHeader\n model(id: $modelId) {\n id\n name\n pendingImportedVersions {\n ...PendingFileUpload\n }\n }\n ...ProjectModelPageVersionsPagination\n ...ProjectsModelPageEmbed_Project\n workspace {\n id\n readOnly\n }\n }\n": types.ProjectModelPageVersionsProjectFragmentDoc,
|
||||
@@ -530,8 +526,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {\n id\n name\n }\n": types.SettingsWorkspaceGeneralDeleteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n }\n": types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n": types.SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceBillingPageNew_Workspace on Workspace {\n id\n ...PricingTable_Workspace\n }\n": types.WorkspaceBillingPageNew_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceBillingPage_Workspace on Workspace {\n id\n role\n }\n": types.WorkspaceBillingPage_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersChangeRoleDialog_Workspace on Workspace {\n id\n plan {\n status\n name\n }\n subscription {\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n }\n": types.SettingsWorkspacesMembersChangeRoleDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
|
||||
@@ -723,8 +718,7 @@ const documents: Documents = {
|
||||
"\n query SettingsSidebar {\n activeUser {\n ...SettingsSidebar_User\n }\n }\n": types.SettingsSidebarDocument,
|
||||
"\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n": types.SettingsSidebarAutomateFunctionsDocument,
|
||||
"\n query SettingsWorkspaceGeneral($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": types.SettingsWorkspaceGeneralDocument,
|
||||
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
|
||||
"\n query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPageNew_Workspace\n }\n }\n": types.SettingsWorkspaceBillingNewDocument,
|
||||
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPage_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
|
||||
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": types.SettingsWorkspaceBillingCustomerPortalDocument,
|
||||
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": types.SettingsWorkspaceRegionsDocument,
|
||||
"\n query SettingsWorkspacesMembers($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
|
||||
@@ -766,7 +760,7 @@ const documents: Documents = {
|
||||
"\n fragment DiscoverableList_Discoverable on User {\n discoverableWorkspaces {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n": types.DiscoverableList_DiscoverableFragmentDoc,
|
||||
"\n fragment DiscoverableList_Requests on User {\n workspaceJoinRequests {\n items {\n id\n status\n workspace {\n id\n name\n logo\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n }\n }\n": types.DiscoverableList_RequestsFragmentDoc,
|
||||
"\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n": types.UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\n }\n }\n }\n": types.WorkspacesPlan_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n usage {\n projectCount\n modelCount\n }\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\n }\n }\n }\n": types.WorkspacesPlan_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspacePlanLimits_Workspace on Workspace {\n id\n projects(limit: 0) {\n totalCount\n items {\n id\n models(limit: 0) {\n totalCount\n }\n }\n }\n plan {\n name\n }\n }\n": types.WorkspacePlanLimits_WorkspaceFragmentDoc,
|
||||
"\n subscription OnWorkspaceProjectsUpdate($slug: String!) {\n workspaceProjectsUpdated(workspaceId: null, workspaceSlug: $slug) {\n projectId\n workspaceId\n type\n project {\n ...ProjectDashboardItem\n }\n }\n }\n ": types.OnWorkspaceProjectsUpdateDocument,
|
||||
"\n fragment WorkspaceHasCustomDataResidency_Workspace on Workspace {\n id\n defaultRegion {\n id\n name\n }\n }\n": types.WorkspaceHasCustomDataResidency_WorkspaceFragmentDoc,
|
||||
@@ -1005,11 +999,11 @@ export function graphql(source: "\n fragment HeaderNavNotificationsWorkspaceInv
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -1018,10 +1012,6 @@ export function graphql(source: "\n fragment InviteDialogProjectWorkspaceMember
|
||||
* 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 InviteDialogProjectWorkspaceMembers_Project on Project {\n id\n ...ProjectPageTeamInternals_Project\n workspace {\n team {\n items {\n ...InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator\n }\n }\n }\n }\n"): (typeof documents)["\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"];
|
||||
/**
|
||||
* 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 PricingTable_Workspace on Workspace {\n id\n role\n }\n"): (typeof documents)["\n fragment PricingTable_Workspace on Workspace {\n id\n role\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1277,11 +1267,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesGeneralEditSlugD
|
||||
/**
|
||||
* 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 SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\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 WorkspaceBillingPageNew_Workspace on Workspace {\n id\n ...PricingTable_Workspace\n }\n"): (typeof documents)["\n fragment WorkspaceBillingPageNew_Workspace on Workspace {\n id\n ...PricingTable_Workspace\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceBillingPage_Workspace on Workspace {\n id\n role\n }\n"): (typeof documents)["\n fragment WorkspaceBillingPage_Workspace on Workspace {\n id\n role\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2049,11 +2035,7 @@ export function graphql(source: "\n query SettingsWorkspaceGeneral($slug: Strin
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\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 query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPageNew_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPageNew_Workspace\n }\n }\n"];
|
||||
export function graphql(source: "\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPage_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...WorkspaceBillingPage_Workspace\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2221,7 +2203,7 @@ export function graphql(source: "\n fragment UseWorkspaceInviteManager_PendingW
|
||||
/**
|
||||
* 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 WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n usage {\n projectCount\n modelCount\n }\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n paymentMethod\n usage {\n projectCount\n modelCount\n }\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n totalCount\n assigned\n viewersCount\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
@@ -28,17 +28,7 @@ export const settingsWorkspaceBillingQuery = graphql(`
|
||||
query SettingsWorkspaceBilling($slug: String!) {
|
||||
workspaceBySlug(slug: $slug) {
|
||||
id
|
||||
...SettingsWorkspacesBilling_Workspace
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
// TODO: Remove old one post-migration
|
||||
export const settingsWorkspaceBillingQueryNew = graphql(`
|
||||
query SettingsWorkspaceBillingNew($slug: String!) {
|
||||
workspaceBySlug(slug: $slug) {
|
||||
id
|
||||
...WorkspaceBillingPageNew_Workspace
|
||||
...WorkspaceBillingPage_Workspace
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -19,6 +19,10 @@ graphql(`
|
||||
createdAt
|
||||
name
|
||||
paymentMethod
|
||||
usage {
|
||||
projectCount
|
||||
modelCount
|
||||
}
|
||||
}
|
||||
subscription {
|
||||
billingInterval
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useMutation } from '@vue/apollo-composable'
|
||||
import { workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { mapMainRoleToGqlWorkspaceRole } from '~/lib/workspaces/helpers/roles'
|
||||
import { mapServerRoleToGqlServerRole } from '~/lib/common/helpers/roles'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { Roles, WorkspacePlans } from '@speckle/shared'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { useNavigation } from '~/lib/navigation/composables/navigation'
|
||||
|
||||
@@ -120,7 +120,7 @@ export const useWorkspacesWizard = () => {
|
||||
mixpanel.stop_session_recording()
|
||||
|
||||
const needsCheckout =
|
||||
wizardState.value.state.plan !== PaidWorkspacePlans.Starter ||
|
||||
wizardState.value.state.plan !== WorkspacePlans.Free ||
|
||||
wizardState.value.state.billingInterval === BillingInterval.Yearly
|
||||
const workspaceId = ref(wizardState.value.state.id)
|
||||
const isNewWorkspace = !workspaceId.value
|
||||
@@ -263,7 +263,7 @@ export const useWorkspacesWizard = () => {
|
||||
}
|
||||
|
||||
if (
|
||||
state.plan === PaidWorkspacePlans.Starter &&
|
||||
state.plan === WorkspacePlans.Free &&
|
||||
state.billingInterval === BillingInterval.Monthly
|
||||
) {
|
||||
triggerNotification({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
BillingInterval,
|
||||
PaidWorkspacePlans,
|
||||
WorkspacePlans,
|
||||
SettingsWorkspacesRegionsSelect_ServerRegionItemFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
@@ -34,7 +34,7 @@ export type WorkspaceWizardState = {
|
||||
name: string
|
||||
slug: string
|
||||
invites: string[]
|
||||
plan: PaidWorkspacePlans | null
|
||||
plan: WorkspacePlans | null
|
||||
billingInterval: BillingInterval | null
|
||||
id: string
|
||||
region: SettingsWorkspacesRegionsSelect_ServerRegionItemFragment | null
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<SettingsWorkspacesBillingPageNew v-if="isWorkspaceNewPlansEnabled && !forceOld" />
|
||||
<SettingsWorkspacesBillingPage v-else />
|
||||
<SettingsWorkspacesBillingPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,8 +12,4 @@ definePageMeta({
|
||||
useHead({
|
||||
title: 'Settings | Workspace - Billing'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
const forceOld = computed(() => route.query.old === 'true')
|
||||
</script>
|
||||
|
||||
@@ -84,11 +84,17 @@ enum WorkspacePaymentMethod {
|
||||
billing
|
||||
}
|
||||
|
||||
type WorkspacePlanUsage {
|
||||
projectCount: Int!
|
||||
modelCount: Int!
|
||||
}
|
||||
|
||||
type WorkspacePlan {
|
||||
name: WorkspacePlans!
|
||||
status: WorkspacePlanStatuses!
|
||||
createdAt: DateTime!
|
||||
paymentMethod: WorkspacePaymentMethod!
|
||||
usage: WorkspacePlanUsage!
|
||||
}
|
||||
|
||||
type WorkspaceSubscriptionSeats {
|
||||
|
||||
@@ -63,6 +63,8 @@ generates:
|
||||
WorkspaceMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceMutationsGraphQLReturn'
|
||||
WorkspaceJoinRequestMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestMutationsGraphQLReturn'
|
||||
WorkspaceInviteMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceInviteMutationsGraphQLReturn'
|
||||
WorkspacePlan: '@/modules/gatekeeperCore/helpers/graphTypes#WorkspacePlanGraphQLReturn'
|
||||
WorkspacePlanUsage: '@/modules/gatekeeperCore/helpers/graphTypes#WorkspacePlanUsageGraphQLReturn'
|
||||
WorkspaceProjectMutations: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceProjectMutationsGraphQLReturn'
|
||||
WorkspaceBillingMutations: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceBillingMutationsGraphQLReturn'
|
||||
PendingWorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#PendingWorkspaceCollaboratorGraphQLReturn'
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/
|
||||
import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types';
|
||||
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
|
||||
import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes';
|
||||
import { WorkspacePlanGraphQLReturn, WorkspacePlanUsageGraphQLReturn, PriceGraphQLReturn } from '@/modules/gatekeeperCore/helpers/graphTypes';
|
||||
import { WorkspaceBillingMutationsGraphQLReturn, WorkspaceSubscriptionGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes';
|
||||
import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes';
|
||||
import { SmartTextEditorValueGraphQLReturn } from '@/modules/core/services/richTextEditorService';
|
||||
@@ -14,7 +15,6 @@ import { ActivityCollectionGraphQLReturn } from '@/modules/activitystream/helper
|
||||
import { ServerAppGraphQLReturn, ServerAppListItemGraphQLReturn } from '@/modules/auth/helpers/graphTypes';
|
||||
import { GendoAIRenderGraphQLReturn } from '@/modules/gendo/helpers/types/graphTypes';
|
||||
import { ServerRegionItemGraphQLReturn } from '@/modules/multiregion/helpers/graphTypes';
|
||||
import { PriceGraphQLReturn } from '@/modules/gatekeeperCore/helpers/graphTypes';
|
||||
import { GraphQLContext } from '@/modules/shared/helpers/typeHelper';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
@@ -4749,6 +4749,7 @@ export type WorkspacePlan = {
|
||||
name: WorkspacePlans;
|
||||
paymentMethod: WorkspacePaymentMethod;
|
||||
status: WorkspacePlanStatuses;
|
||||
usage: WorkspacePlanUsage;
|
||||
};
|
||||
|
||||
export type WorkspacePlanPrice = {
|
||||
@@ -4768,6 +4769,12 @@ export const WorkspacePlanStatuses = {
|
||||
} as const;
|
||||
|
||||
export type WorkspacePlanStatuses = typeof WorkspacePlanStatuses[keyof typeof WorkspacePlanStatuses];
|
||||
export type WorkspacePlanUsage = {
|
||||
__typename?: 'WorkspacePlanUsage';
|
||||
modelCount: Scalars['Int']['output'];
|
||||
projectCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Business: 'business',
|
||||
@@ -5353,9 +5360,10 @@ export type ResolversTypes = {
|
||||
WorkspaceMutations: ResolverTypeWrapper<WorkspaceMutationsGraphQLReturn>;
|
||||
WorkspacePaymentMethod: WorkspacePaymentMethod;
|
||||
WorkspacePermissionChecks: ResolverTypeWrapper<WorkspacePermissionChecksGraphQLReturn>;
|
||||
WorkspacePlan: ResolverTypeWrapper<WorkspacePlan>;
|
||||
WorkspacePlan: ResolverTypeWrapper<WorkspacePlanGraphQLReturn>;
|
||||
WorkspacePlanPrice: ResolverTypeWrapper<Omit<WorkspacePlanPrice, 'monthly' | 'yearly'> & { monthly?: Maybe<ResolversTypes['Price']>, yearly?: Maybe<ResolversTypes['Price']> }>;
|
||||
WorkspacePlanStatuses: WorkspacePlanStatuses;
|
||||
WorkspacePlanUsage: ResolverTypeWrapper<WorkspacePlanUsageGraphQLReturn>;
|
||||
WorkspacePlans: WorkspacePlans;
|
||||
WorkspaceProjectCreateInput: WorkspaceProjectCreateInput;
|
||||
WorkspaceProjectInviteCreateInput: WorkspaceProjectInviteCreateInput;
|
||||
@@ -5652,8 +5660,9 @@ export type ResolversParentTypes = {
|
||||
WorkspaceMembersByRole: WorkspaceMembersByRole;
|
||||
WorkspaceMutations: WorkspaceMutationsGraphQLReturn;
|
||||
WorkspacePermissionChecks: WorkspacePermissionChecksGraphQLReturn;
|
||||
WorkspacePlan: WorkspacePlan;
|
||||
WorkspacePlan: WorkspacePlanGraphQLReturn;
|
||||
WorkspacePlanPrice: Omit<WorkspacePlanPrice, 'monthly' | 'yearly'> & { monthly?: Maybe<ResolversParentTypes['Price']>, yearly?: Maybe<ResolversParentTypes['Price']> };
|
||||
WorkspacePlanUsage: WorkspacePlanUsageGraphQLReturn;
|
||||
WorkspaceProjectCreateInput: WorkspaceProjectCreateInput;
|
||||
WorkspaceProjectInviteCreateInput: WorkspaceProjectInviteCreateInput;
|
||||
WorkspaceProjectMutations: WorkspaceProjectMutationsGraphQLReturn;
|
||||
@@ -7302,6 +7311,7 @@ export type WorkspacePlanResolvers<ContextType = GraphQLContext, ParentType exte
|
||||
name?: Resolver<ResolversTypes['WorkspacePlans'], ParentType, ContextType>;
|
||||
paymentMethod?: Resolver<ResolversTypes['WorkspacePaymentMethod'], ParentType, ContextType>;
|
||||
status?: Resolver<ResolversTypes['WorkspacePlanStatuses'], ParentType, ContextType>;
|
||||
usage?: Resolver<ResolversTypes['WorkspacePlanUsage'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
@@ -7312,6 +7322,12 @@ export type WorkspacePlanPriceResolvers<ContextType = GraphQLContext, ParentType
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspacePlanUsageResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspacePlanUsage'] = ResolversParentTypes['WorkspacePlanUsage']> = {
|
||||
modelCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
projectCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceProjectMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceProjectMutations'] = ResolversParentTypes['WorkspaceProjectMutations']> = {
|
||||
create?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsCreateArgs, 'input'>>;
|
||||
moveToRegion?: Resolver<ResolversTypes['String'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsMoveToRegionArgs, 'projectId' | 'regionKey'>>;
|
||||
@@ -7549,6 +7565,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
WorkspacePermissionChecks?: WorkspacePermissionChecksResolvers<ContextType>;
|
||||
WorkspacePlan?: WorkspacePlanResolvers<ContextType>;
|
||||
WorkspacePlanPrice?: WorkspacePlanPriceResolvers<ContextType>;
|
||||
WorkspacePlanUsage?: WorkspacePlanUsageResolvers<ContextType>;
|
||||
WorkspaceProjectMutations?: WorkspaceProjectMutationsResolvers<ContextType>;
|
||||
WorkspaceProjectsUpdatedMessage?: WorkspaceProjectsUpdatedMessageResolvers<ContextType>;
|
||||
WorkspaceRoleCollection?: WorkspaceRoleCollectionResolvers<ContextType>;
|
||||
|
||||
@@ -4729,6 +4729,7 @@ export type WorkspacePlan = {
|
||||
name: WorkspacePlans;
|
||||
paymentMethod: WorkspacePaymentMethod;
|
||||
status: WorkspacePlanStatuses;
|
||||
usage: WorkspacePlanUsage;
|
||||
};
|
||||
|
||||
export type WorkspacePlanPrice = {
|
||||
@@ -4748,6 +4749,12 @@ export const WorkspacePlanStatuses = {
|
||||
} as const;
|
||||
|
||||
export type WorkspacePlanStatuses = typeof WorkspacePlanStatuses[keyof typeof WorkspacePlanStatuses];
|
||||
export type WorkspacePlanUsage = {
|
||||
__typename?: 'WorkspacePlanUsage';
|
||||
modelCount: Scalars['Int']['output'];
|
||||
projectCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Business: 'business',
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
import {
|
||||
countWorkspaceRoleWithOptionalProjectRoleFactory,
|
||||
getWorkspaceFactory,
|
||||
getWorkspaceRoleForUserFactory
|
||||
getWorkspaceRoleForUserFactory,
|
||||
getWorkspacesProjectsCountsFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { db } from '@/db/knex'
|
||||
@@ -71,6 +72,10 @@ import {
|
||||
import { assignWorkspaceSeatFactory } from '@/modules/workspaces/services/workspaceSeat'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { getTotalSeatsCountByPlanFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
|
||||
import { legacyGetStreamsFactory } from '@/modules/core/repositories/streams'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { getStreamBranchCountFactory } from '@/modules/core/repositories/branches'
|
||||
|
||||
const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } =
|
||||
getFeatureFlags()
|
||||
@@ -188,6 +193,41 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
})
|
||||
} as unknown as WorkspaceSeatsByType)
|
||||
},
|
||||
WorkspacePlan: {
|
||||
usage: async (parent) => {
|
||||
return { workspaceId: parent.workspaceId }
|
||||
}
|
||||
},
|
||||
WorkspacePlanUsage: {
|
||||
projectCount: async (parent) => {
|
||||
const { workspaceId } = parent
|
||||
const countsByWorkspaceId = await getWorkspacesProjectsCountsFactory({ db })({
|
||||
workspaceIds: [workspaceId]
|
||||
})
|
||||
return countsByWorkspaceId[workspaceId] ?? 0
|
||||
},
|
||||
modelCount: async (parent) => {
|
||||
const { workspaceId } = parent
|
||||
|
||||
let modelCount = 0
|
||||
|
||||
const queryAllWorkspaceProjects = queryAllWorkspaceProjectsFactory({
|
||||
getStreams: legacyGetStreamsFactory({ db })
|
||||
})
|
||||
|
||||
for await (const projects of queryAllWorkspaceProjects({ workspaceId })) {
|
||||
for (const project of projects) {
|
||||
const regionDb = await getProjectDbClient({ projectId: project.id })
|
||||
const projectModelCount = await getStreamBranchCountFactory({
|
||||
db: regionDb
|
||||
})(project.id)
|
||||
modelCount = modelCount + projectModelCount
|
||||
}
|
||||
}
|
||||
|
||||
return modelCount
|
||||
}
|
||||
},
|
||||
WorkspaceSubscription: {
|
||||
seats: async (parent) => {
|
||||
const workspacePlan = await getWorkspacePlanFactory({ db })({
|
||||
|
||||
+73
@@ -20,6 +20,7 @@ import {
|
||||
} from '@/test/authHelper'
|
||||
import {
|
||||
GetWorkspaceDocument,
|
||||
GetWorkspacePlanUsageDocument,
|
||||
GetWorkspaceWithSeatsByTypeDocument,
|
||||
GetWorkspaceWithSubscriptionDocument
|
||||
} from '@/test/graphql/generated/graphql'
|
||||
@@ -29,6 +30,8 @@ import {
|
||||
TestApolloServer
|
||||
} from '@/test/graphqlHelper'
|
||||
import { beforeEachContext } from '@/test/hooks'
|
||||
import { createTestBranches } from '@/test/speckle-helpers/branchHelper'
|
||||
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
@@ -335,4 +338,74 @@ describe('Workspaces Billing', () => {
|
||||
})
|
||||
}
|
||||
)
|
||||
;(FF_WORKSPACES_MODULE_ENABLED ? describe : describe.skip)(
|
||||
'workspace.subscription.usage',
|
||||
async () => {
|
||||
const user = await createTestUser({
|
||||
name: createRandomString(),
|
||||
email: createRandomEmail(),
|
||||
role: Roles.Server.Admin,
|
||||
verified: true
|
||||
})
|
||||
const workspace = {
|
||||
id: createRandomString(),
|
||||
name: createRandomString(),
|
||||
slug: cryptoRandomString({ length: 10 }),
|
||||
ownerId: user.id
|
||||
}
|
||||
await createTestWorkspace(workspace, user, {
|
||||
addPlan: { name: 'pro', status: 'valid' }
|
||||
})
|
||||
|
||||
const project: BasicTestStream = {
|
||||
id: createRandomString(),
|
||||
name: createRandomString(),
|
||||
ownerId: user.id,
|
||||
isPublic: true
|
||||
}
|
||||
await createTestStream(project, user)
|
||||
await createTestBranches([
|
||||
{
|
||||
owner: user,
|
||||
stream: project,
|
||||
branch: {
|
||||
id: createRandomString(),
|
||||
streamId: project.id,
|
||||
authorId: user.id,
|
||||
name: createRandomString()
|
||||
}
|
||||
},
|
||||
{
|
||||
owner: user,
|
||||
stream: project,
|
||||
branch: {
|
||||
id: createRandomString(),
|
||||
streamId: project.id,
|
||||
authorId: user.id,
|
||||
name: createRandomString()
|
||||
}
|
||||
},
|
||||
{
|
||||
owner: user,
|
||||
stream: project,
|
||||
branch: {
|
||||
id: createRandomString(),
|
||||
streamId: project.id,
|
||||
authorId: user.id,
|
||||
name: createRandomString()
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const session = await login(user)
|
||||
|
||||
const res = await session.execute(GetWorkspacePlanUsageDocument, {
|
||||
workspaceId: workspace.id
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res?.data?.workspace?.plan?.usage?.projectCount).to.equal(31)
|
||||
expect(res?.data?.workspace?.plan?.usage?.modelCount).to.equal(3)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,3 +1,7 @@
|
||||
import { Price } from '@/modules/core/graph/generated/graphql'
|
||||
import { WorkspacePlan } from '@speckle/shared'
|
||||
|
||||
export type PriceGraphQLReturn = Omit<Price, 'currencySymbol'>
|
||||
|
||||
export type WorkspacePlanGraphQLReturn = WorkspacePlan & { workspaceId: string }
|
||||
export type WorkspacePlanUsageGraphQLReturn = { workspaceId: string }
|
||||
|
||||
@@ -45,7 +45,7 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
const results = await getWorkspacesProjectsCounts({
|
||||
workspaceIds: ids.slice()
|
||||
})
|
||||
return ids.map((id) => results[id] ?? null)
|
||||
return ids.map((id) => results[id])
|
||||
})
|
||||
},
|
||||
workspaceDomains: {
|
||||
|
||||
@@ -35,7 +35,11 @@ import {
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import { Knex } from 'knex'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
|
||||
import {
|
||||
BranchRecord,
|
||||
StreamAclRecord,
|
||||
StreamRecord
|
||||
} from '@/modules/core/helpers/types'
|
||||
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
|
||||
import {
|
||||
WorkspaceAcl as DbWorkspaceAcl,
|
||||
@@ -63,6 +67,7 @@ import {
|
||||
} from '@/modules/workspaces/domain/types'
|
||||
|
||||
const tables = {
|
||||
branches: (db: Knex) => db<BranchRecord>('branches'),
|
||||
streams: (db: Knex) => db<StreamRecord>('streams'),
|
||||
streamAcl: (db: Knex) => db<StreamAclRecord>('stream_acl'),
|
||||
workspaces: (db: Knex) => db<Workspace>('workspaces'),
|
||||
@@ -512,8 +517,8 @@ export const upsertWorkspaceCreationStateFactory =
|
||||
|
||||
export const getWorkspacesProjectsCountsFactory =
|
||||
(deps: { db: Knex }): GetWorkspacesProjectsCounts =>
|
||||
async (params) => {
|
||||
const ret = params.workspaceIds.reduce((acc, workspaceId) => {
|
||||
async ({ workspaceIds }) => {
|
||||
const ret = workspaceIds.reduce((acc, workspaceId) => {
|
||||
acc[workspaceId] = 0
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
@@ -526,7 +531,7 @@ export const getWorkspacesProjectsCountsFactory =
|
||||
count: string
|
||||
}[]
|
||||
>([Streams.col.workspaceId, knex.raw('count(*) as count')])
|
||||
.whereIn(Streams.col.workspaceId, params.workspaceIds)
|
||||
.whereIn(Streams.col.workspaceId, workspaceIds)
|
||||
.groupBy(Streams.col.workspaceId)
|
||||
|
||||
const res = await q
|
||||
|
||||
@@ -402,6 +402,20 @@ export const getWorkspaceWithSeatsByType = gql`
|
||||
${basicWorkspaceFragment}
|
||||
`
|
||||
|
||||
export const getWorkspacePlanUsage = gql`
|
||||
query GetWorkspacePlanUsage($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
...BasicWorkspace
|
||||
plan {
|
||||
usage {
|
||||
projectCount
|
||||
modelCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getWorkspaceWithMembersByRole = gql`
|
||||
query GetWorkspaceWithMembersByRole($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
|
||||
@@ -4730,6 +4730,7 @@ export type WorkspacePlan = {
|
||||
name: WorkspacePlans;
|
||||
paymentMethod: WorkspacePaymentMethod;
|
||||
status: WorkspacePlanStatuses;
|
||||
usage: WorkspacePlanUsage;
|
||||
};
|
||||
|
||||
export type WorkspacePlanPrice = {
|
||||
@@ -4749,6 +4750,12 @@ export const WorkspacePlanStatuses = {
|
||||
} as const;
|
||||
|
||||
export type WorkspacePlanStatuses = typeof WorkspacePlanStatuses[keyof typeof WorkspacePlanStatuses];
|
||||
export type WorkspacePlanUsage = {
|
||||
__typename?: 'WorkspacePlanUsage';
|
||||
modelCount: Scalars['Int']['output'];
|
||||
projectCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Business: 'business',
|
||||
@@ -5263,6 +5270,13 @@ export type GetWorkspaceWithSeatsByTypeQueryVariables = Exact<{
|
||||
|
||||
export type GetWorkspaceWithSeatsByTypeQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: string, createdAt: string, role?: string | null, readOnly: boolean, seatsByType?: { __typename?: 'WorkspaceSeatsByType', editors?: { __typename?: 'WorkspaceSeatCollection', totalCount: number } | null, viewers?: { __typename?: 'WorkspaceSeatCollection', totalCount: number } | null } | null } };
|
||||
|
||||
export type GetWorkspacePlanUsageQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetWorkspacePlanUsageQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: string, createdAt: string, role?: string | null, readOnly: boolean, plan?: { __typename?: 'WorkspacePlan', usage: { __typename?: 'WorkspacePlanUsage', projectCount: number, modelCount: number } } | null } };
|
||||
|
||||
export type GetWorkspaceWithMembersByRoleQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}>;
|
||||
@@ -6120,6 +6134,7 @@ export const RequestToJoinWorkspaceDocument = {"kind":"Document","definitions":[
|
||||
export const GetWorkspaceWithJoinRequestsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithJoinRequests"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"AdminWorkspaceJoinRequestFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"adminWorkspacesJoinRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithJoinRequestsQuery, GetWorkspaceWithJoinRequestsQueryVariables>;
|
||||
export const GetWorkspaceWithSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"currentBillingCycleEnd"}},{"kind":"Field","name":{"kind":"Name","value":"billingInterval"}},{"kind":"Field","name":{"kind":"Name","value":"seats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"guest"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}},{"kind":"Field","name":{"kind":"Name","value":"assigned"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"viewersCount"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithSubscriptionQuery, GetWorkspaceWithSubscriptionQueryVariables>;
|
||||
export const GetWorkspaceWithSeatsByTypeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithSeatsByType"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"seatsByType"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"editors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"viewers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithSeatsByTypeQuery, GetWorkspaceWithSeatsByTypeQueryVariables>;
|
||||
export const GetWorkspacePlanUsageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspacePlanUsage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"plan"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"usage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCount"}},{"kind":"Field","name":{"kind":"Name","value":"modelCount"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspacePlanUsageQuery, GetWorkspacePlanUsageQueryVariables>;
|
||||
export const GetWorkspaceWithMembersByRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithMembersByRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"membersByRole"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"guests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithMembersByRoleQuery, GetWorkspaceWithMembersByRoleQueryVariables>;
|
||||
export const UpdateWorkspaceProjectRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceProjectRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectUpdateRoleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<UpdateWorkspaceProjectRoleMutation, UpdateWorkspaceProjectRoleMutationVariables>;
|
||||
export const UpdateWorkspaceSeatTypeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceSeatType"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateSeatTypeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSeatType"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpdateWorkspaceSeatTypeMutation, UpdateWorkspaceSeatTypeMutationVariables>;
|
||||
|
||||
Reference in New Issue
Block a user