Merge branch 'main' into andrew/web-3448-enable-auto-joining-a-workspace-as-a-setting
This commit is contained in:
+13
-12
@@ -20,7 +20,7 @@ aliases:
|
||||
- &yarn
|
||||
run:
|
||||
name: Install Dependencies
|
||||
command: PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn
|
||||
command: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn
|
||||
|
||||
workflows:
|
||||
test-build:
|
||||
@@ -433,7 +433,9 @@ jobs:
|
||||
working_directory: *work-dir
|
||||
steps:
|
||||
- checkout
|
||||
- *yarn
|
||||
- run:
|
||||
name: Install Hardened Dependencies
|
||||
command: YARN_ENABLE_HARDENED_MODE=1 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn
|
||||
- run:
|
||||
name: Build public packages
|
||||
command: yarn build:public
|
||||
@@ -550,15 +552,14 @@ jobs:
|
||||
name: Introspect GQL schema for subsequent checks
|
||||
command: 'IGNORE_MISSING_MIRATIONS=true yarn cli graphql introspect'
|
||||
working_directory: 'packages/server'
|
||||
# Temporarily disabled while Apollo Studio is down - https://status.apollographql.com/
|
||||
# - run:
|
||||
# name: Checking for GQL schema breakages against app.speckle.systems
|
||||
# command: 'yarn rover graph check Speckle-Server@app-speckle-systems --schema ./introspected-schema.graphql'
|
||||
# working_directory: 'packages/server'
|
||||
# - run:
|
||||
# name: Checking for GQL schema breakages against latest.speckle.systems
|
||||
# command: 'yarn rover graph check Speckle-Server@latest-speckle-systems --schema ./introspected-schema.graphql'
|
||||
# working_directory: 'packages/server'
|
||||
- run:
|
||||
name: Checking for GQL schema breakages against app.speckle.systems
|
||||
command: 'yarn rover graph check Speckle-Server@app-speckle-systems --schema ./introspected-schema.graphql'
|
||||
working_directory: 'packages/server'
|
||||
- run:
|
||||
name: Checking for GQL schema breakages against latest.speckle.systems
|
||||
command: 'yarn rover graph check Speckle-Server@latest-speckle-systems --schema ./introspected-schema.graphql'
|
||||
working_directory: 'packages/server'
|
||||
- store_test_results:
|
||||
path: packages/server/reports
|
||||
|
||||
@@ -774,7 +775,7 @@ jobs:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: PUPPETEER_SKIP_DOWNLOAD=true yarn
|
||||
command: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true yarn
|
||||
- run:
|
||||
name: Build public packages
|
||||
command: yarn build:public
|
||||
|
||||
@@ -56,12 +56,15 @@ import { useInviteUserToWorkspace } from '~/lib/workspaces/composables/managemen
|
||||
import { getRoleLabel } from '~~/lib/settings/helpers/utils'
|
||||
import { matchesDomainPolicy } from '~/lib/invites/helpers/validation'
|
||||
import { useInviteUserToProject } from '~~/lib/projects/composables/projectManagement'
|
||||
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
|
||||
|
||||
graphql(`
|
||||
fragment InviteDialogWorkspace_Workspace on Workspace {
|
||||
id
|
||||
name
|
||||
slug
|
||||
domainBasedMembershipProtectionEnabled
|
||||
defaultSeatType
|
||||
domains {
|
||||
domain
|
||||
id
|
||||
@@ -78,6 +81,9 @@ const mixpanel = useMixpanel()
|
||||
const inviteToWorkspace = useInviteUserToWorkspace()
|
||||
const inviteToProject = useInviteUserToProject()
|
||||
|
||||
const workspaceSlug = computed(() => props.workspace?.slug || '')
|
||||
const { isSelfServePlan } = useWorkspacePlan(workspaceSlug.value)
|
||||
|
||||
const isSelectingRole = ref(true)
|
||||
const selectedRole = ref<WorkspaceRoles>(Roles.Workspace.Member)
|
||||
const project = ref<FormSelectProjects_ProjectFragment>()
|
||||
@@ -119,9 +125,21 @@ const allowedDomains = computed(() =>
|
||||
)
|
||||
const infoText = computed(() => {
|
||||
if (selectedRole.value === Roles.Workspace.Member) {
|
||||
return 'Inviting is free. Members join your workspace on a free Viewer seat. You can give them an Editor seat later if they need to contribute to projects beyond viewing and commenting.'
|
||||
if (props.workspace?.defaultSeatType === 'editor') {
|
||||
if (isSelfServePlan.value) {
|
||||
return `Members join your workspace on an Editor seat by default. Editor seats may incur charges based on your workspace plan. You can change their seat type to Viewer after they join if they only need to view and comment.`
|
||||
}
|
||||
return `Members join your workspace on an Editor seat by default. Editor seats have additional permissions compared to Viewer seats. You can change their seat type to Viewer after they join if they only need to view and comment.`
|
||||
}
|
||||
return `Inviting is free. Members join your workspace on a free Viewer seat. You can give them an Editor seat later if they need to contribute to projects beyond viewing and commenting.`
|
||||
}
|
||||
|
||||
if (props.workspace?.defaultSeatType === 'editor') {
|
||||
if (isSelfServePlan.value) {
|
||||
return `Guests join your workspace on an Editor seat by default. Editor seats may incur charges based on your workspace plan. You can change their seat type to Viewer after they join if they only need to view and comment.`
|
||||
}
|
||||
return `Guests join your workspace on an Editor seat by default. Editor seats have additional permissions compared to Viewer seats. You can change their seat type to Viewer after they join if they only need to view and comment.`
|
||||
}
|
||||
return `Inviting is free. Guests join your workspace on a free Viewer seat. You can give them an Editor seat later if they need to contribute to a project beyond viewing and commenting.`
|
||||
})
|
||||
|
||||
|
||||
@@ -280,9 +280,10 @@ onMounted(() => {
|
||||
|
||||
// Watch for plan limit conditions and show dialog if needed
|
||||
watch(
|
||||
[hasMissingReferencedObject],
|
||||
([missingObject]) => {
|
||||
if (missingObject) {
|
||||
[hasMissingReferencedObject, state.resources.response.resourcesLoading],
|
||||
([missingObject, resourcesLoading]: [boolean, boolean]) => {
|
||||
// Only show dialog if resources are not loading to prevent flashing during version switches
|
||||
if (missingObject && !resourcesLoading) {
|
||||
if (isFederated.value) {
|
||||
limitsDialogType.value = 'federated'
|
||||
} else {
|
||||
|
||||
@@ -26,7 +26,8 @@ export const formatName = (plan?: MaybeNullOrUndefined<WorkspacePlans>) => {
|
||||
[WorkspacePlans.TeamUnlimitedInvoiced]: 'Starter (Invoiced)',
|
||||
[WorkspacePlans.Pro]: 'Business',
|
||||
[WorkspacePlans.ProUnlimited]: 'Business',
|
||||
[WorkspacePlans.ProUnlimitedInvoiced]: 'Business (Invoiced)'
|
||||
[WorkspacePlans.ProUnlimitedInvoiced]: 'Business (Invoiced)',
|
||||
[WorkspacePlans.Enterprise]: 'Enterprise'
|
||||
}
|
||||
return formattedPlanNames[plan]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ 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 workspaceSlug\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 }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\n domains {\n domain\n id\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\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 query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n invitableCollaborators(filter: $filter) {\n items {\n user {\n id\n name\n }\n }\n }\n }\n }\n": typeof types.InviteDialogProjectRowProjectCollaboratorsDocument,
|
||||
"\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Project\n }\n": typeof types.ProjectCardImportFileArea_ProjectFragmentDoc,
|
||||
@@ -495,7 +495,7 @@ 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 workspaceSlug\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 }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\n domains {\n domain\n id\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
|
||||
"\n query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n invitableCollaborators(filter: $filter) {\n items {\n user {\n id\n name\n }\n }\n }\n }\n }\n": types.InviteDialogProjectRowProjectCollaboratorsDocument,
|
||||
"\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Project\n }\n": types.ProjectCardImportFileArea_ProjectFragmentDoc,
|
||||
@@ -1065,7 +1065,7 @@ 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 }\n"): (typeof documents)["\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\n domains {\n domain\n id\n }\n }\n"): (typeof documents)["\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\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.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,51 +2,51 @@ import type { TutorialItem } from '~/lib/dashboard/helpers/types'
|
||||
|
||||
export const tutorialItems: TutorialItem[] = [
|
||||
{
|
||||
title: 'Get Civil 3D Pipe Networks Into Revit as Families',
|
||||
title: 'Embed 3D models in Miro',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/67b8980920e42b89aff75a3f_C3d%20to%20Revit.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/pipe-networks-civil3d-revit'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/6840300850bf20bbc4598692_Embed%20in%20Miro.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/embed-3d-models-in-miro'
|
||||
},
|
||||
{
|
||||
title: 'How To Get Data From Grasshopper Into Power BI',
|
||||
title: 'Embed 3D models in Notion',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/677d34c3f1e3b99aa578bfea_GH%20to%20PBI%20tutorial-p-800.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/best-way-to-get-data-from-grasshopper-into-power-bi'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/68402d8d2220374a63dc4164_Embed%20in%20Notion.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/how-to-embed-3d-models-in-notion'
|
||||
},
|
||||
{
|
||||
title: 'Automating CFD Analysis with Speckle',
|
||||
title: 'Visualize Archicad models in 3D with Power BI',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c508e14c68996f7aa1f5_66d648086cd656dae5168a85_Tutorial-Graphics-automate-centeres-cfd-1.png',
|
||||
url: 'https://www.speckle.systems/tutorials/automating-cfd-analysis-with-speckle'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/68345b16d7223d552bf58f84_Archicad%20to%20%20Power%20BI.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/visualize-archicad-models-in-3d-with-power-bi'
|
||||
},
|
||||
{
|
||||
title: 'PowerQuery(QL) for AEC Data Analysis',
|
||||
title: 'Visualize Rhino models in 3D with Power BI',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c49549279bcda21c3115_66e047f5c915694aefb78105_powerquery-specklecon%25400.5x.png',
|
||||
url: 'https://www.speckle.systems/tutorials/powerquery-ql-for-aec-data-analysis'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/68345a0a3271258ec8a1dacc_Rhino%20to%20%20Power%20BI.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/visualize-rhino-models-in-3d-with-power-bi'
|
||||
},
|
||||
{
|
||||
title: 'From Grasshopper Placeholders to Unreal Assets',
|
||||
title: 'Visualize SketchUp models in 3D with Power BI',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c50919d477e7b4454747_66e047f5b7f992018acdb66b_grasshopper-unreal%25400.5x.png',
|
||||
url: 'https://www.speckle.systems/tutorials/from-grasshopper-placeholders-to-unreal-assets'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/68345976fda085cd403855c0_SketchUp%20to%20%20Power%20BI.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/visualize-sketchup-models-in-3d-with-power-bi'
|
||||
},
|
||||
{
|
||||
title: 'Rhino Block to Revit Family',
|
||||
title: 'Visualize Tekla models in 3D with Power BI',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c50a49279bcda21c9483_66e047f58da87f021da53e06_rhino-2-rvt-families%25400.5x.png',
|
||||
url: 'https://www.speckle.systems/tutorials/rhino-block-to-revit-family'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/683458947403486c2b2235ac_Tekla%20to%20%20Power%20BI.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/visualize-tekla-models-in-3d-with-power-bi'
|
||||
},
|
||||
{
|
||||
title: 'SketchUp Components to Revit Families',
|
||||
title: 'Visualize Navisworks models in 3D with Power BI',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c50ac6269f3166039982_66e047f51d1cb84078e7320a_skp-2-rvt-families%25400.5x.png',
|
||||
url: 'https://www.speckle.systems/tutorials/sketchup-components-to-revit-families'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/6834573d14d3d379e7c0ffa9_Navisworks%20to%20%20Power%20BI.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/visualize-navisworks-models-in-3d-with-power-bi'
|
||||
},
|
||||
{
|
||||
title: 'Block to Family Conversion with Speckle',
|
||||
title: 'Visualize ETABS models in 3D with Power BI',
|
||||
image:
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/66f9c50ac50a28b58fe0e10b_66e047f505114bd4a6854b6a_216-blocks-to-families%25400.5x.png',
|
||||
url: 'https://www.speckle.systems/tutorials/new-in-2-16-block-to-family-conversion'
|
||||
'https://cdn.prod.website-files.com/66c31b5a50432200dc753cc4/683454bb49e49e15ec574877_ETABS%20to%20%20Power%20BI.jpg',
|
||||
url: 'https://www.speckle.systems/tutorials/visualize-etabs-models-in-3d-with-power-bi'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -217,6 +217,7 @@ export type InjectableViewerState = Readonly<{
|
||||
* Fetch the next page of versions for a loaded model
|
||||
*/
|
||||
loadMoreVersions: (modelId: string) => Promise<void>
|
||||
resourcesLoading: ComputedRef<boolean>
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -713,6 +714,7 @@ function setupResponseResourceData(
|
||||
// sorting variables so that we don't refetech just because the order changed
|
||||
const {
|
||||
result: viewerLoadedResourcesResult,
|
||||
loading: viewerLoadedResourcesLoading,
|
||||
variables: viewerLoadedResourcesVariables,
|
||||
onError: onViewerLoadedResourcesError,
|
||||
onResult: onViewerLoadedResourcesResult,
|
||||
@@ -880,7 +882,8 @@ function setupResponseResourceData(
|
||||
resourceQueryVariables: computed(() => viewerLoadedResourcesVariables.value),
|
||||
threadsQueryVariables: computed(() => threadsQueryVariables.value),
|
||||
loadMoreVersions,
|
||||
resourcesLoaded: computed(() => initLoadDone.value)
|
||||
resourcesLoaded: computed(() => initLoadDone.value),
|
||||
resourcesLoading: computed(() => viewerLoadedResourcesLoading.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, test, expect, beforeEach } from 'vitest'
|
||||
import { DefermentManager } from './defermentManager.js'
|
||||
import { DefermentManagerOptions } from '../operations/options.js'
|
||||
import { Base, Item } from '../types/types.js'
|
||||
|
||||
const makeItem = (id: string, size = 1): Item => ({
|
||||
baseId: id,
|
||||
base: { foo: 'bar' } as unknown as Base,
|
||||
size
|
||||
})
|
||||
|
||||
describe('DefermentManager totalDefermentRequests', () => {
|
||||
let manager: DefermentManager
|
||||
let options: DefermentManagerOptions
|
||||
|
||||
beforeEach(() => {
|
||||
options = { maxSizeInMb: 1, ttlms: 1000, logger: (): void => {} }
|
||||
manager = new DefermentManager(options)
|
||||
})
|
||||
|
||||
test('tracks deferment requests for each id', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
manager.defer({ id: 'a' })
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
manager.defer({ id: 'a' })
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
manager.defer({ id: 'b' })
|
||||
// @ts-expect-error: access private for test
|
||||
expect(manager.totalDefermentRequests.get('a')).toBe(2)
|
||||
// @ts-expect-error: access private for test
|
||||
expect(manager.totalDefermentRequests.get('b')).toBe(1)
|
||||
})
|
||||
|
||||
test('increments and does not reset on undefer', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
manager.defer({ id: 'x' })
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
manager.defer({ id: 'x' })
|
||||
manager.undefer(makeItem('x'))
|
||||
// @ts-expect-error: access private for test
|
||||
expect(manager.totalDefermentRequests.get('x')).toBe(2)
|
||||
// @ts-expect-error: access private for test
|
||||
const deferredBase = manager.deferments.get('x')
|
||||
expect(deferredBase).toBeDefined()
|
||||
expect(deferredBase?.getId()).toBe('x')
|
||||
})
|
||||
|
||||
test('does not increment for undefer only', () => {
|
||||
manager.undefer(makeItem('y'))
|
||||
// @ts-expect-error: access private for test
|
||||
expect(manager.totalDefermentRequests.get('y')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -4,13 +4,14 @@ import { DefermentManagerOptions } from '../operations/options.js'
|
||||
|
||||
export class DefermentManager {
|
||||
private deferments: Map<string, DeferredBase> = new Map()
|
||||
private timer?: ReturnType<typeof setTimeout>
|
||||
private logger: CustomLogger
|
||||
private currentSize = 0
|
||||
private disposed = false
|
||||
//tracks total deferment requests for each id
|
||||
//this is used to prevent cleaning up deferments that are still being requested
|
||||
private totalDefermentRequests: Map<string, number> = new Map()
|
||||
|
||||
constructor(private options: DefermentManagerOptions) {
|
||||
this.resetGlobalTimer()
|
||||
this.logger = options.logger || ((): void => {})
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ export class DefermentManager {
|
||||
|
||||
async defer(params: { id: string }): Promise<Base> {
|
||||
if (this.disposed) throw new Error('DefermentManager is disposed')
|
||||
this.trackDefermentRequest(params.id)
|
||||
const now = this.now()
|
||||
const deferredBase = this.deferments.get(params.id)
|
||||
if (deferredBase) {
|
||||
@@ -44,10 +46,27 @@ export class DefermentManager {
|
||||
return notYetFound.getPromise()
|
||||
}
|
||||
|
||||
private trackDefermentRequest(id: string): void {
|
||||
const request = this.totalDefermentRequests.get(id)
|
||||
if (request) {
|
||||
this.totalDefermentRequests.set(id, request + 1)
|
||||
} else {
|
||||
this.totalDefermentRequests.set(id, 1)
|
||||
}
|
||||
}
|
||||
|
||||
undefer(item: Item): void {
|
||||
if (this.disposed) throw new Error('DefermentManager is disposed')
|
||||
const now = this.now()
|
||||
this.currentSize += item.size || 0
|
||||
if (this.currentSize > this.options.maxSizeInMb * 1024 * 1024) {
|
||||
this.logger(
|
||||
'deferments size exceeded, cleaning up',
|
||||
this.currentSize,
|
||||
this.options.maxSizeInMb
|
||||
)
|
||||
this.cleanDeferments()
|
||||
}
|
||||
//order matters here with found before undefer
|
||||
const deferredBase = this.deferments.get(item.baseId)
|
||||
if (deferredBase) {
|
||||
@@ -60,21 +79,9 @@ export class DefermentManager {
|
||||
}
|
||||
}
|
||||
|
||||
private resetGlobalTimer(): void {
|
||||
const run = (): void => {
|
||||
this.cleanDeferments()
|
||||
this.timer = setTimeout(run, this.options.ttlms)
|
||||
}
|
||||
this.timer = setTimeout(run, this.options.ttlms)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) return
|
||||
this.disposed = true
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = undefined
|
||||
}
|
||||
this.clearDeferments()
|
||||
}
|
||||
|
||||
@@ -92,15 +99,6 @@ export class DefermentManager {
|
||||
}
|
||||
|
||||
private cleanDeferments(): void {
|
||||
const maxSizeBytes = this.options.maxSizeInMb * 1024 * 1024
|
||||
if (this.currentSize < maxSizeBytes) {
|
||||
this.logger(
|
||||
'deferments size is ok, no need to clean',
|
||||
this.currentSize,
|
||||
maxSizeBytes
|
||||
)
|
||||
return
|
||||
}
|
||||
const now = this.now()
|
||||
let cleaned = 0
|
||||
const start = performance.now()
|
||||
@@ -108,16 +106,19 @@ export class DefermentManager {
|
||||
.filter((x) => x.isExpired(now))
|
||||
.sort((a, b) => this.compareMaybeBasesBySize(a.getItem(), b.getItem()))) {
|
||||
if (deferredBase.done(now)) {
|
||||
//if the deferment is done but has been requested multiple times,
|
||||
//we do not clean it up to allow the requests to resolve
|
||||
const requestCount = this.totalDefermentRequests.get(deferredBase.getId())
|
||||
if (requestCount && requestCount > 1) {
|
||||
return
|
||||
}
|
||||
this.currentSize -= deferredBase.getItem()?.size || 0
|
||||
this.deferments.delete(deferredBase.getId())
|
||||
cleaned++
|
||||
if (this.currentSize < maxSizeBytes) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
this.logger(
|
||||
'cleaned deferments, cleaned, left',
|
||||
'cleaned deferments: cleaned, left, time',
|
||||
cleaned,
|
||||
this.deferments.size,
|
||||
performance.now() - start
|
||||
|
||||
@@ -41,7 +41,7 @@ export class ObjectLoader2 {
|
||||
this.#database = options.database
|
||||
this.#deferments = new DefermentManager({
|
||||
maxSizeInMb: 2_000, // 2 GBs
|
||||
ttlms: 5_000, // 5 seconds
|
||||
ttlms: 15_000, // 15 seconds
|
||||
logger: this.#logger
|
||||
})
|
||||
this.#cache = new CacheReader(this.#database, this.#deferments, cacheOptions)
|
||||
@@ -94,7 +94,11 @@ export class ObjectLoader2 {
|
||||
yield rootItem.base
|
||||
if (!rootItem.base.__closure) return
|
||||
|
||||
const children = Object.keys(rootItem.base.__closure)
|
||||
//sort the closures by their values descending
|
||||
const sortedClosures = Object.entries(rootItem.base.__closure).sort(
|
||||
(a, b) => b[1] - a[1]
|
||||
)
|
||||
const children = sortedClosures.map((x) => x[0])
|
||||
const total = children.length
|
||||
this.#downloader.initializePool({
|
||||
results: new AggregateQueue(this.#gathered, this.#pump),
|
||||
|
||||
@@ -64,6 +64,7 @@ enum WorkspacePlans {
|
||||
pro
|
||||
proUnlimited
|
||||
proUnlimitedInvoiced
|
||||
enterprise
|
||||
}
|
||||
|
||||
enum WorkspacePlanStatuses {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
upsertWorkspacePlanFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { PaidWorkspacePlans, PaidWorkspacePlanStatuses } from '@speckle/shared'
|
||||
import { WorkspacePlans, WorkspacePlanStatuses } from '@speckle/shared'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans'
|
||||
|
||||
@@ -18,8 +18,9 @@ const command: CommandModule<
|
||||
unknown,
|
||||
{
|
||||
workspaceSlugOrId: string
|
||||
status: PaidWorkspacePlanStatuses
|
||||
plan: PaidWorkspacePlans
|
||||
// you need to know what you are doing, status and plan pairing validity is not ensured here
|
||||
status: WorkspacePlanStatuses
|
||||
plan: WorkspacePlans
|
||||
}
|
||||
> = {
|
||||
command: 'set-plan <workspaceSlugOrId> [plan] [status]',
|
||||
@@ -32,8 +33,8 @@ const command: CommandModule<
|
||||
plan: {
|
||||
describe: 'Plan to set the status for',
|
||||
type: 'string',
|
||||
default: PaidWorkspacePlans.Team,
|
||||
choices: [PaidWorkspacePlans.Team, PaidWorkspacePlans.Pro]
|
||||
default: WorkspacePlans.Team,
|
||||
choices: Object.values(WorkspacePlans)
|
||||
},
|
||||
status: {
|
||||
describe: 'Status to set for the workspace plan',
|
||||
|
||||
@@ -4954,6 +4954,7 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Enterprise: 'enterprise',
|
||||
Free: 'free',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
|
||||
@@ -4934,6 +4934,7 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Enterprise: 'enterprise',
|
||||
Free: 'free',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { getFeatureFlags, getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { Roles, throwUncoveredError, WorkspacePlanFeatures } from '@speckle/shared'
|
||||
import {
|
||||
Roles,
|
||||
throwUncoveredError,
|
||||
WorkspacePlanFeatures,
|
||||
WorkspacePlans
|
||||
} from '@speckle/shared'
|
||||
import {
|
||||
getWorkspaceFactory,
|
||||
getWorkspaceRoleForUserFactory,
|
||||
@@ -71,19 +76,20 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
if (!workspacePlan) return null
|
||||
let paymentMethod: WorkspacePaymentMethod
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
paymentMethod = WorkspacePaymentMethod.Billing
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'free':
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.Free:
|
||||
paymentMethod = WorkspacePaymentMethod.Unpaid
|
||||
break
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.Enterprise:
|
||||
paymentMethod = WorkspacePaymentMethod.Invoice
|
||||
break
|
||||
default:
|
||||
@@ -248,17 +254,18 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
if (subscription) {
|
||||
let purchased = 0
|
||||
switch (workspacePlan.name) {
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'free':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.Enterprise:
|
||||
// not stripe paid plans and old plans do not have seats available
|
||||
break
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
purchased = getTotalSeatsCountByPlanFactory({
|
||||
getWorkspacePlanProductId
|
||||
})({
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
PaidWorkspacePlanStatuses,
|
||||
throwUncoveredError
|
||||
throwUncoveredError,
|
||||
WorkspacePlans
|
||||
} from '@speckle/shared'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations'
|
||||
@@ -78,16 +79,17 @@ export const handleSubscriptionUpdateFactory =
|
||||
|
||||
if (status) {
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'free':
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.Enterprise:
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
@@ -146,21 +148,22 @@ export const addWorkspaceSubscriptionSeatIfNeededFactory =
|
||||
// if (!workspaceSubscription) throw new WorkspaceSubscriptionNotFoundError()
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
// If viewer seat type, we don't need to do anything
|
||||
if (seatType === WorkspaceSeatType.Viewer) {
|
||||
return
|
||||
} else {
|
||||
break
|
||||
}
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'free':
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.Enterprise:
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
|
||||
+11
-10
@@ -15,7 +15,7 @@ import {
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { mutateSubscriptionDataWithNewValidSeatNumbers } from '@/modules/gatekeeper/services/subscriptions/mutateSubscriptionDataWithNewValidSeatNumbers'
|
||||
import { Logger } from '@/observability/logging'
|
||||
import { throwUncoveredError } from '@speckle/shared'
|
||||
import { throwUncoveredError, WorkspacePlans } from '@speckle/shared'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
|
||||
type DownscaleWorkspaceSubscription = (args: {
|
||||
@@ -41,16 +41,17 @@ export const downscaleWorkspaceSubscriptionFactory =
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'free':
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.Enterprise:
|
||||
case WorkspacePlans.Unlimited:
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
|
||||
+12
-10
@@ -27,7 +27,8 @@ import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
throwUncoveredError,
|
||||
WorkspacePlanBillingIntervals
|
||||
WorkspacePlanBillingIntervals,
|
||||
WorkspacePlans
|
||||
} from '@speckle/shared'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
@@ -68,16 +69,17 @@ export const upgradeWorkspaceSubscriptionFactory =
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'free': // Upgrade from free is handled through startCheckout since it is from free to paid
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.Enterprise:
|
||||
case WorkspacePlans.Free: // Upgrade from free is handled through startCheckout since it is from free to paid
|
||||
throw new WorkspaceNotPaidPlanError()
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
|
||||
@@ -9,7 +9,8 @@ const WorkspacePlansUpgradeMapping: Record<WorkspacePlans, WorkspacePlans[]> = {
|
||||
teamUnlimitedInvoiced: [],
|
||||
pro: ['pro', 'proUnlimited'],
|
||||
proUnlimited: ['proUnlimited'],
|
||||
proUnlimitedInvoiced: []
|
||||
proUnlimitedInvoiced: [],
|
||||
enterprise: []
|
||||
}
|
||||
|
||||
export const isUpgradeWorkspacePlanValid = ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { InvalidWorkspacePlanStatus } from '@/modules/gatekeeper/errors/billing'
|
||||
import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import { GetWorkspace } from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { throwUncoveredError, WorkspacePlan } from '@speckle/shared'
|
||||
import { throwUncoveredError, WorkspacePlan, WorkspacePlans } from '@speckle/shared'
|
||||
|
||||
export const updateWorkspacePlanFactory =
|
||||
({
|
||||
@@ -35,10 +35,10 @@ export const updateWorkspacePlanFactory =
|
||||
const createdAt = new Date()
|
||||
const updatedAt = new Date()
|
||||
switch (name) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
switch (status) {
|
||||
case 'valid':
|
||||
case 'cancelationScheduled':
|
||||
@@ -53,11 +53,12 @@ export const updateWorkspacePlanFactory =
|
||||
}
|
||||
break
|
||||
|
||||
case 'free':
|
||||
case 'academia':
|
||||
case 'unlimited':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Enterprise:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
switch (status) {
|
||||
case 'valid':
|
||||
await upsertWorkspacePlan({
|
||||
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
} from '@/modules/workspaces/services/retrieval'
|
||||
import {
|
||||
Roles,
|
||||
WorkspacePlans,
|
||||
WorkspaceRoles,
|
||||
removeNullOrUndefinedKeys,
|
||||
throwUncoveredError
|
||||
@@ -707,10 +708,10 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
const workspacePlan = await getWorkspacePlanFactory({ db })({ workspaceId })
|
||||
if (workspacePlan) {
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
switch (workspacePlan.status) {
|
||||
case 'cancelationScheduled':
|
||||
case 'valid':
|
||||
@@ -721,11 +722,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
case 'free':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.Enterprise:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
|
||||
@@ -27,9 +27,9 @@ export const withOperationLogging = async <T>(
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
OperationStatus.start,
|
||||
{ ...OperationStatus.start, operationDescription },
|
||||
`${OperationLogLinePrefix}${
|
||||
operationDescription ? ` ${operationDescription}` : ''
|
||||
operationDescription ? ' {operationDescription}' : ''
|
||||
}`
|
||||
)
|
||||
const results = await operation()
|
||||
|
||||
@@ -4935,6 +4935,7 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Enterprise: 'enterprise',
|
||||
Free: 'free',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
|
||||
@@ -148,6 +148,17 @@ export const WorkspacePaidPlanConfigs: {
|
||||
export const WorkspaceUnpaidPlanConfigs: {
|
||||
[plan in UnpaidWorkspacePlans]: WorkspacePlanConfig<plan>
|
||||
} = {
|
||||
[UnpaidWorkspacePlans.Enterprise]: {
|
||||
plan: UnpaidWorkspacePlans.Enterprise,
|
||||
features: [
|
||||
...baseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.Unlimited]: {
|
||||
plan: UnpaidWorkspacePlans.Unlimited,
|
||||
features: [
|
||||
|
||||
@@ -18,7 +18,8 @@ describe('plan helpers', () => {
|
||||
proUnlimitedInvoiced: false,
|
||||
team: false,
|
||||
teamUnlimited: true,
|
||||
teamUnlimitedInvoiced: false
|
||||
teamUnlimitedInvoiced: false,
|
||||
enterprise: false
|
||||
}
|
||||
it.each(Object.entries(planCases))(
|
||||
'plan %s include the paid unlimited projects addon -> %s',
|
||||
@@ -41,7 +42,8 @@ describe('plan helpers', () => {
|
||||
proUnlimitedInvoiced: false,
|
||||
team: true,
|
||||
teamUnlimited: true,
|
||||
teamUnlimitedInvoiced: false
|
||||
teamUnlimitedInvoiced: false,
|
||||
enterprise: false
|
||||
}
|
||||
it.each(Object.entries(planCases))(
|
||||
'is plan %s available self served -> %s',
|
||||
|
||||
@@ -13,6 +13,7 @@ export type PaidWorkspacePlans =
|
||||
export const UnpaidWorkspacePlans = <const>{
|
||||
TeamUnlimitedInvoiced: 'teamUnlimitedInvoiced',
|
||||
ProUnlimitedInvoiced: 'proUnlimitedInvoiced',
|
||||
Enterprise: 'enterprise',
|
||||
Unlimited: 'unlimited',
|
||||
Academia: 'academia',
|
||||
Free: 'free'
|
||||
@@ -32,16 +33,17 @@ export const doesPlanIncludeUnlimitedProjectsAddon = (
|
||||
plan: WorkspacePlans
|
||||
): boolean => {
|
||||
switch (plan) {
|
||||
case 'teamUnlimited':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
return true
|
||||
case 'free':
|
||||
case 'team':
|
||||
case 'pro':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.Enterprise:
|
||||
return false
|
||||
|
||||
default:
|
||||
@@ -51,16 +53,17 @@ export const doesPlanIncludeUnlimitedProjectsAddon = (
|
||||
|
||||
export const isSelfServeAvailablePlan = (plan: WorkspacePlans): boolean => {
|
||||
switch (plan) {
|
||||
case 'free':
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
return true
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.Enterprise:
|
||||
return false
|
||||
|
||||
default:
|
||||
@@ -70,16 +73,17 @@ export const isSelfServeAvailablePlan = (plan: WorkspacePlans): boolean => {
|
||||
|
||||
export const isPaidPlan = (plan: WorkspacePlans): boolean => {
|
||||
switch (plan) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case WorkspacePlans.Team:
|
||||
case WorkspacePlans.TeamUnlimited:
|
||||
case WorkspacePlans.Pro:
|
||||
case WorkspacePlans.ProUnlimited:
|
||||
return true
|
||||
case 'free':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case WorkspacePlans.Free:
|
||||
case WorkspacePlans.TeamUnlimitedInvoiced:
|
||||
case WorkspacePlans.ProUnlimitedInvoiced:
|
||||
case WorkspacePlans.Unlimited:
|
||||
case WorkspacePlans.Academia:
|
||||
case WorkspacePlans.Enterprise:
|
||||
return false
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user