Merge branch 'main' into andrew/upgrade-explode-extension

This commit is contained in:
andrewwallacespeckle
2025-09-23 18:36:11 +02:00
12 changed files with 38 additions and 64 deletions
@@ -18,10 +18,10 @@ const description = computed(() => {
if (!props.planName) return ''
if (props.planName === WorkspacePlans.Team)
return 'Your workspace has reached a usage limit. To keep using Speckle, you can buy the Unlimited projects and models add-on to your current Starter plan, or upgrade to the Business plan.'
return 'Your workspace has reached a usage limit. Upgrade to the Business plan to keep using Speckle.'
if (props.planName === WorkspacePlans.Pro)
return 'Your workspace has reached a usage limit. To keep using Speckle, you can buy the Unlimited projects and models add-on to your current Business plan.'
return 'Your workspace has reached a usage limit. Buy the Unlimited projects and models add-on to keep using Speckle.'
return 'Your workspace has reached a usage limit. To keep using Speckle, you should upgrade your workspace to the Starter or Business plan. Note that both plans can be extended with the Unlimited projects and models add-on.'
return 'Your workspace has reached a usage limit. To keep using Speckle, you should upgrade your workspace to the Starter or Business plan.'
})
</script>
@@ -18,7 +18,6 @@ const props = defineProps<{
isYearlyIntervalSelected: boolean
currency?: Currency
tooltip?: string
fixedPrice?: string
}>()
const { addonPrices } = useWorkspaceAddonPrices()
@@ -40,10 +39,6 @@ const addonPrice = computed(() => {
})
const text = computed(() => {
if (props.fixedPrice) {
return props.fixedPrice
}
return addonPrice.value
? `${addonPrice.value} per editor seat / month`
: 'Contact us for pricing'
@@ -64,7 +64,7 @@
:description="featureMetadata.description"
/>
</ul>
<div v-if="showAddons && displayAddons.length > 0" class="mt-auto lg:h-72 pt-8">
<div v-if="showAddons && displayAddons.length > 0" class="mt-auto lg:h-32 pt-8">
<h5 class="text-body-2xs mb-2 text-foreground-2">Available add-ons</h5>
<div class="flex flex-col gap-y-2">
<PricingTableAddon
@@ -75,7 +75,6 @@
:is-yearly-interval-selected="isYearlyIntervalSelected"
:currency="props.currency"
:tooltip="addon.tooltip"
:fixed-price="addon.fixedPrice"
/>
</div>
</div>
@@ -145,16 +144,16 @@ const commonFeatures = shallowRef([
planLimits.value.projectCount === 1 ? '' : 's'
}`,
description:
props.plan === WorkspacePlans.Free
? 'Your maximum number of projects'
: 'Your maximum number of projects. Can be extended with the Unlimited projects and models add-on.'
props.plan === WorkspacePlans.Pro
? 'Your maximum number of projects. Can be extended with the Unlimited projects and models add-on.'
: 'Your maximum number of projects'
},
{
displayName: `${planLimits.value.modelCount} models per workspace`,
description:
props.plan === WorkspacePlans.Free
? 'Your maximum number of models'
: 'Your maximum number of models. Can be extended with the Unlimited projects and models add-on.'
props.plan === WorkspacePlans.Pro
? 'Your maximum number of models. Can be extended with the Unlimited projects and models add-on.'
: 'Your maximum number of models'
},
{
displayName: planLimits.value.versionsHistory
@@ -384,30 +383,13 @@ const badgeText = computed(() =>
)
const displayAddons = computed(() => {
if (props.plan === WorkspacePlans.Team) {
if (props.plan === WorkspacePlans.Pro) {
return [
{
title: 'Unlimited projects and models',
tooltip: 'You can purchase this in the next step'
}
]
} else if (props.plan === WorkspacePlans.Pro) {
return [
{
title: 'Unlimited projects and models',
tooltip: 'You can purchase this in the next step'
},
{
title: 'Extra data regions',
fixedPrice: '$500 per region / month',
tooltip: 'Available upon request'
},
{
title: 'Priority support',
fixedPrice: 'Contact us for pricing',
tooltip: 'Available upon request'
}
]
}
return []
})
@@ -9,6 +9,7 @@
name="includeArchived"
:value="true"
label="Include resolved"
label-position="right"
/>
</div>
</div>
@@ -4,7 +4,7 @@
title="Unlimited projects and models"
:subtitle="`${addonPrice} per editor/month`"
info="Power through with unlimited projects and models in your workspace."
disclaimer="Only on Starter & Business plans"
disclaimer="Only on Business plan"
:button="unlimitedAddOnButton"
/>
@@ -51,7 +51,7 @@ const props = defineProps<{
}>()
const {
isPaidPlan,
isBusinessPlan,
currency,
plan,
intervalIsYearly,
@@ -78,14 +78,14 @@ const addOnButtonTooltip = computed(() => {
return 'You must be a workspace admin in order to purchase the add-on'
if (hasUnlimitedAddon.value)
return 'The add-on is already included in your subscription'
if (!isPaidPlan.value) return 'Only available for Starter & Business plans'
if (!isBusinessPlan.value) return 'Only available for the Business plan'
return undefined
})
const unlimitedAddOnButton = computed(() => ({
text: 'Buy add-on',
id: 'buy-add-on',
disabled: !isPaidPlan.value || hasUnlimitedAddon.value || !isAdmin.value,
disabled: !isBusinessPlan.value || hasUnlimitedAddon.value || !isAdmin.value,
disabledMessage: addOnButtonTooltip.value,
onClick: () => {
isUpgradeDialogOpen.value = true
@@ -62,7 +62,7 @@ const mixpanel = useMixpanel()
const { projectCount, modelCount } = useWorkspaceUsage(props.slug)
const featureFlags = useFeatureFlags()
const showAddonSelect = ref<boolean>(true)
const showAddonSelect = ref<boolean>(false)
const isLoading = ref<boolean>(false)
const title = computed(() => {
@@ -196,7 +196,12 @@ watch(
() => isOpen.value,
(newVal) => {
if (newVal) {
showAddonSelect.value = props.isChangingPlan && !isSamePlanWithAddon.value
// Only show addon select for Business (Pro) plans
const isBusinessPlan =
props.plan === PaidWorkspacePlans.Pro ||
props.plan === PaidWorkspacePlans.ProUnlimited
showAddonSelect.value =
props.isChangingPlan && !isSamePlanWithAddon.value && isBusinessPlan
// If the add-on is required or already included, set it to yes
if (usageExceedsNewPlanLimit.value && props.isChangingPlan) {
includeUnlimitedAddon.value = 'yes'
@@ -15,7 +15,7 @@
:name="`select-all-${selectedCount}-${totalCount}`"
:model-value="areAllValuesSelected"
:indeterminate="areSomeValuesSelected"
class="pointer-events-none -mt-1"
class="pointer-events-none"
:class="selectAllCheckboxClasses"
hide-label
/>
@@ -32,7 +32,6 @@
</template>
<script setup lang="ts">
import { FormCheckbox } from '@speckle/ui-components'
import { useFilterUtilities } from '~/lib/viewer/composables/filtering/filtering'
import type { StringFilterData } from '~/lib/viewer/helpers/filters/types'
@@ -2,9 +2,9 @@
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events -->
<template>
<div class="px-1">
<div class="px-1 h-full">
<div
class="group/checkbox flex items-center justify-between gap-2 text-body-3xs py-0.5 px-2 hover:bg-highlight-1 rounded-md cursor-pointer"
class="group/checkbox flex items-center justify-between gap-2 text-body-3xs py-0.5 px-2 hover:bg-highlight-1 rounded-md cursor-pointer h-full"
@click="$emit('toggle')"
@mouseenter="highlight"
@mouseleave="unhighlight"
@@ -12,7 +12,7 @@
<div class="flex items-center min-w-0">
<!-- Checkbox is purely visual - so pointer-events-none -->
<FormCheckbox
class="pointer-events-none -mt-1"
class="pointer-events-none"
:class="{
'border-transparent group-hover/checkbox:border-outline-5': !isSelected,
'opacity-50 dark:!bg-transparent !border dark:!border-outline-5 !group-hover/checkbox:border-outline-5':
@@ -42,7 +42,6 @@
</template>
<script setup lang="ts">
import { FormCheckbox } from '@speckle/ui-components'
import { useFilterUtilities } from '~/lib/viewer/composables/filtering/filtering'
import { getFilterValueCount } from '~/lib/viewer/helpers/filters/utils'
import type { StringFilterData } from '~/lib/viewer/helpers/filters/types'
@@ -104,11 +104,6 @@ onResult((result) => {
newState.plan === PaidWorkspacePlans.ProUnlimited
) {
goToStep(WizardSteps.Region)
} else if (
newState.plan === PaidWorkspacePlans.Team ||
newState.plan === PaidWorkspacePlans.TeamUnlimited
) {
goToStep(WizardSteps.AddOns)
} else {
goToStep(WizardSteps.Pricing)
}
@@ -92,15 +92,9 @@ export const useWorkspacesWizard = () => {
let shouldComplete = false
if (wizardState.value.currentStep === WizardSteps.Pricing) {
if (state.value.plan === WorkspacePlans.Free) {
shouldComplete = true
}
}
if (wizardState.value.currentStep === WizardSteps.AddOns) {
if (
state.value.plan === WorkspacePlans.Team ||
state.value.plan === WorkspacePlans.TeamUnlimited
state.value.plan === WorkspacePlans.Free ||
state.value.plan === WorkspacePlans.Team
) {
shouldComplete = true
}
@@ -1,13 +1,17 @@
<template>
<div
class="relative flex"
:class="labelPosition === 'left' ? 'flex-row-reverse items-center' : 'items-start'"
class="relative flex items-center"
:class="[
labelPosition === 'left' && 'flex-row-reverse items-center',
labelPosition === 'top' && 'items-start',
labelPosition === 'right' && 'items-center'
]"
>
<div
class="flex items-center h-6"
class="flex items-center h-3.5 w-3.5"
:class="labelPosition === 'left' ? 'w-1/2 justify-end mr-2' : ''"
>
<div class="relative w-3.5 h-3.5">
<div class="relative flex h-full w-full">
<input
:id="finalId"
:checked="coreChecked"
@@ -4,7 +4,7 @@ import { clientOs, ModifierKeys } from '~~/src/helpers/form/input'
import { computed, ref } from 'vue'
import type { Ref } from 'vue'
export type LabelPosition = 'top' | 'left'
export type LabelPosition = 'top' | 'left' | 'right'
/**
* onKeyDown wrapper that also checks for modifier keys being pressed