diff --git a/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql b/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql index 4b779ca52..a0cc1ec5f 100644 --- a/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql +++ b/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql @@ -92,8 +92,20 @@ type WorkspacePlan { } type WorkspaceSubscriptionSeats { - plan: Int! - guest: Int! + plan: Int! @deprecated + guest: Int! @deprecated + """ + Total number of seats purchased and available in the current subscription cycle + """ + totalCount: Int! + """ + Number assigned seats in the current billing cycle + """ + assigned: Int! + """ + Number of viewer seats currently assigned in the workspace + """ + viewersCount: Int! } type WorkspaceSubscription { diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index aa4f4b8bd..bee2e7612 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -84,6 +84,7 @@ generates: ServerRegionMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' ServerRegionItem: '@/modules/multiregion/helpers/graphTypes#ServerRegionItemGraphQLReturn' Price: '@/modules/gatekeeperCore/helpers/graphTypes#PriceGraphQLReturn' + WorkspaceSubscription: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionGraphQLReturn' modules/cross-server-sync/graph/generated/graphql.ts: plugins: - 'typescript' diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 7d69f1c19..f1fc11a9e 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -6,7 +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 } from '@/modules/workspacesCore/helpers/graphTypes'; -import { WorkspaceBillingMutationsGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes'; +import { WorkspaceBillingMutationsGraphQLReturn, WorkspaceSubscriptionGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes'; import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes'; import { SmartTextEditorValueGraphQLReturn } from '@/modules/core/services/richTextEditorService'; import { BlobStorageItem } from '@/modules/blobstorage/domain/types'; @@ -4878,8 +4878,16 @@ export type WorkspaceSubscription = { export type WorkspaceSubscriptionSeats = { __typename?: 'WorkspaceSubscriptionSeats'; + /** Number assigned seats in the current billing cycle */ + assigned: Scalars['Int']['output']; + /** @deprecated Field no longer supported */ guest: Scalars['Int']['output']; + /** @deprecated Field no longer supported */ plan: Scalars['Int']['output']; + /** Total number of seats purchased and available in the current subscription cycle */ + totalCount: Scalars['Int']['output']; + /** Number of viewer seats currently assigned in the workspace */ + viewersCount: Scalars['Int']['output']; }; export type WorkspaceTeamFilter = { @@ -5297,7 +5305,7 @@ export type ResolversTypes = { WorkspaceSso: ResolverTypeWrapper; WorkspaceSsoProvider: ResolverTypeWrapper; WorkspaceSsoSession: ResolverTypeWrapper; - WorkspaceSubscription: ResolverTypeWrapper; + WorkspaceSubscription: ResolverTypeWrapper; WorkspaceSubscriptionSeats: ResolverTypeWrapper; WorkspaceTeamFilter: WorkspaceTeamFilter; WorkspaceUpdateInput: WorkspaceUpdateInput; @@ -5583,7 +5591,7 @@ export type ResolversParentTypes = { WorkspaceSso: WorkspaceSsoGraphQLReturn; WorkspaceSsoProvider: WorkspaceSsoProvider; WorkspaceSsoSession: WorkspaceSsoSession; - WorkspaceSubscription: WorkspaceSubscription; + WorkspaceSubscription: WorkspaceSubscriptionGraphQLReturn; WorkspaceSubscriptionSeats: WorkspaceSubscriptionSeats; WorkspaceTeamFilter: WorkspaceTeamFilter; WorkspaceUpdateInput: WorkspaceUpdateInput; @@ -7241,8 +7249,11 @@ export type WorkspaceSubscriptionResolvers = { + assigned?: Resolver; guest?: Resolver; plan?: Resolver; + totalCount?: Resolver; + viewersCount?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 222bb3687..16c042570 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -4858,8 +4858,16 @@ export type WorkspaceSubscription = { export type WorkspaceSubscriptionSeats = { __typename?: 'WorkspaceSubscriptionSeats'; + /** Number assigned seats in the current billing cycle */ + assigned: Scalars['Int']['output']; + /** @deprecated Field no longer supported */ guest: Scalars['Int']['output']; + /** @deprecated Field no longer supported */ plan: Scalars['Int']['output']; + /** Total number of seats purchased and available in the current subscription cycle */ + totalCount: Scalars['Int']['output']; + /** Number of viewer seats currently assigned in the workspace */ + viewersCount: Scalars['Int']['output']; }; export type WorkspaceTeamFilter = { diff --git a/packages/server/modules/gatekeeper/graph/resolvers/index.ts b/packages/server/modules/gatekeeper/graph/resolvers/index.ts index 694037a67..b906d5e9d 100644 --- a/packages/server/modules/gatekeeper/graph/resolvers/index.ts +++ b/packages/server/modules/gatekeeper/graph/resolvers/index.ts @@ -67,6 +67,7 @@ import { } from '@/modules/gatekeeper/repositories/workspaceSeat' import { assignWorkspaceSeatFactory } from '@/modules/workspaces/services/workspaceSeat' import { getEventBus } from '@/modules/shared/services/eventBus' +import { getTotalSeatsCountByPlanFactory } from '@/modules/gatekeeper/services/subscriptions' const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags() @@ -158,6 +159,45 @@ export = FF_GATEKEEPER_MODULE_ENABLED }) } }, + WorkspaceSubscription: { + seats: async (parent) => { + const workspacePlan = await getWorkspacePlanFactory({ db })({ + workspaceId: parent.workspaceId + }) + if (!workspacePlan || !isNewPlanType(workspacePlan.name)) { + return { + ...calculateSubscriptionSeats({ + subscriptionData: parent.subscriptionData, + guestSeatProductId: getWorkspacePlanProductId({ + workspacePlan: 'guest' + }) + }), + // These values have no reference in the old plans FF_WORKSPACES_NEW_PLANS_ENABLED + totalCount: 0, + assigned: 0 + } + } + // Only editor seats are considered + const assignedSeatsCount = await countSeatsByTypeInWorkspaceFactory({ db })({ + workspaceId: parent.workspaceId, + type: 'editor' + }) + return { + assigned: assignedSeatsCount, + totalCount: getTotalSeatsCountByPlanFactory({ getWorkspacePlanProductId })({ + workspacePlan, + subscriptionData: parent.subscriptionData + }), + viewersCount: await countSeatsByTypeInWorkspaceFactory({ db })({ + workspaceId: parent.workspaceId, + type: 'viewer' + }), + // These values have no reference in the new plans + guest: 0, + plan: 0 + } + } + }, WorkspaceCollaborator: { seatType: async (parent, _args, context) => { const seat = await context.loaders diff --git a/packages/server/modules/gatekeeper/helpers/graphTypes.ts b/packages/server/modules/gatekeeper/helpers/graphTypes.ts index 90b501e55..96e3e23c6 100644 --- a/packages/server/modules/gatekeeper/helpers/graphTypes.ts +++ b/packages/server/modules/gatekeeper/helpers/graphTypes.ts @@ -1,3 +1,8 @@ import { MutationsObjectGraphQLReturn } from '@/modules/core/helpers/graphTypes' +import { WorkspaceSubscription } from '@/modules/gatekeeper/domain/billing' +import { Workspace } from '@/modules/workspacesCore/domain/types' export type WorkspaceBillingMutationsGraphQLReturn = MutationsObjectGraphQLReturn +export type WorkspaceSubscriptionGraphQLReturn = WorkspaceSubscription & { + parent: Workspace +} diff --git a/packages/server/modules/gatekeeper/services/subscriptions.ts b/packages/server/modules/gatekeeper/services/subscriptions.ts index 6440c5e7d..fecd34783 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions.ts @@ -26,6 +26,7 @@ import { } from '@speckle/shared' import { cloneDeep, sum } from 'lodash' import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations' +import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing' export const handleSubscriptionUpdateFactory = ({ @@ -292,3 +293,28 @@ export const addWorkspaceSubscriptionSeatIfNeededFactoryOld = prorationBehavior: 'create_prorations' }) } + +export const getTotalSeatsCountByPlanFactory = + ({ + getWorkspacePlanProductId + }: { + getWorkspacePlanProductId: GetWorkspacePlanProductId + }) => + ({ + workspacePlan, + subscriptionData + }: { + workspacePlan: Pick + subscriptionData: Pick + }) => { + if (workspacePlan.name === 'free') { + return 3 // Max editors seats in the free plan + } + const productId = getWorkspacePlanProductId({ + workspacePlan: workspacePlan.name as 'pro' | 'team' + }) + const product = subscriptionData.products.find( + (product) => product.productId === productId + ) + return product?.quantity ?? 0 + } diff --git a/packages/server/modules/gatekeeper/tests/intergration/workspace.graph.spec.ts b/packages/server/modules/gatekeeper/tests/intergration/workspace.graph.spec.ts index 685e44b37..d874c76aa 100644 --- a/packages/server/modules/gatekeeper/tests/intergration/workspace.graph.spec.ts +++ b/packages/server/modules/gatekeeper/tests/intergration/workspace.graph.spec.ts @@ -1,12 +1,27 @@ +import { db } from '@/db/knex' import { AllScopes } from '@/modules/core/helpers/mainConstants' +import { + createRandomEmail, + createRandomString +} from '@/modules/core/helpers/testHelpers' +import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing' +import { upsertWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/repositories/billing' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' -import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation' +import { + assignToWorkspace, + createTestWorkspace +} from '@/modules/workspaces/tests/helpers/creation' import { BasicTestUser, createAuthTokenForUser, - createTestUsers + createTestUser, + createTestUsers, + login } from '@/test/authHelper' -import { GetWorkspaceDocument } from '@/test/graphql/generated/graphql' +import { + GetWorkspaceDocument, + GetWorkspaceWithSubscriptionDocument +} from '@/test/graphql/generated/graphql' import { createTestContext, testApolloServer, @@ -16,6 +31,7 @@ import { beforeEachContext } from '@/test/hooks' import { Roles } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' +import dayjs from 'dayjs' const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags() @@ -103,4 +119,136 @@ describe('Workspaces Billing', () => { }) } ) + ;(FF_BILLING_INTEGRATION_ENABLED ? describe : describe.skip)( + 'workspace.subscription', + () => { + describe('subscription.seats', () => { + it('should return the number of assigned seats', 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' } + }) + await upsertWorkspaceSubscriptionFactory({ db })({ + workspaceSubscription: { + workspaceId: workspace.id, + createdAt: new Date(), + updatedAt: new Date(), + currentBillingCycleEnd: dayjs().add(1, 'month').toDate(), + billingInterval: 'monthly', + subscriptionData: { + subscriptionId: cryptoRandomString({ length: 10 }), + customerId: cryptoRandomString({ length: 10 }), + cancelAt: null, + status: 'active', + currentPeriodEnd: new Date(), + products: [ + { + priceId: createRandomString(), + quantity: 12, + productId: createRandomString(), + subscriptionItemId: createRandomString() + } + ] + } + } + }) + const session = await login(user) + + const res = await session.execute(GetWorkspaceWithSubscriptionDocument, { + workspaceId: workspace.id + }) + + expect(res).to.not.haveGraphQLErrors() + const seats = res.data?.workspace.subscription?.seats + expect(seats?.assigned).to.eq(1) + }) + it('should return the number of viewers', 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' } + }) + await upsertWorkspaceSubscriptionFactory({ db })({ + workspaceSubscription: { + workspaceId: workspace.id, + createdAt: new Date(), + updatedAt: new Date(), + currentBillingCycleEnd: dayjs().add(1, 'month').toDate(), + billingInterval: 'monthly', + subscriptionData: { + subscriptionId: cryptoRandomString({ length: 10 }), + customerId: cryptoRandomString({ length: 10 }), + cancelAt: null, + status: 'active', + currentPeriodEnd: new Date(), + products: [ + { + priceId: createRandomString(), + quantity: 12, + productId: createRandomString(), + subscriptionItemId: createRandomString() + } + ] + } + } + }) + const viewer1 = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + await assignToWorkspace( + workspace, + viewer1, + Roles.Workspace.Member, + WorkspaceSeatType.Viewer + ) + const viewer2 = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + await assignToWorkspace( + workspace, + viewer2, + Roles.Workspace.Member, + WorkspaceSeatType.Viewer + ) + + const session = await login(user) + + const res = await session.execute(GetWorkspaceWithSubscriptionDocument, { + workspaceId: workspace.id + }) + + expect(res).to.not.haveGraphQLErrors() + const seats = res.data?.workspace.subscription?.seats + expect(seats?.viewersCount).to.eq(2) + }) + }) + } + ) }) diff --git a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts index a630ea3f4..50f287a58 100644 --- a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts @@ -15,6 +15,7 @@ import { import { addWorkspaceSubscriptionSeatIfNeededFactoryNew, addWorkspaceSubscriptionSeatIfNeededFactoryOld, + getTotalSeatsCountByPlanFactory, handleSubscriptionUpdateFactory } from '@/modules/gatekeeper/services/subscriptions' import { @@ -2350,4 +2351,40 @@ describe('subscriptions @gatekeeper', () => { expect(newProduct!.priceId).to.equal('newPlanPrice') }) }) + describe('getTotalSeatsCountByPlanFactory returns a function that, ', () => { + it('should return the fixed value for the free plan', () => { + const getWorkspacePlanProductId = () => expect.fail() + expect( + getTotalSeatsCountByPlanFactory({ getWorkspacePlanProductId })({ + workspacePlan: { name: 'free' }, + subscriptionData: { products: [] } + }) + ).to.eq(3) + }) + it('should return 0 if subscription data has no product', () => { + const getWorkspacePlanProductId = () => 'any' + expect( + getTotalSeatsCountByPlanFactory({ getWorkspacePlanProductId })({ + workspacePlan: { name: 'pro' }, + subscriptionData: { products: [] } + }) + ).to.eq(0) + }) + it('should return the number of purchased seats in the current billing period for the subscription', () => { + const getWorkspacePlanProductId = () => 'productId' + expect( + getTotalSeatsCountByPlanFactory({ getWorkspacePlanProductId })({ + workspacePlan: { name: 'pro' }, + subscriptionData: { + products: [ + { + productId: 'productId', + quantity: 4 + } as SubscriptionData['products'][number] + ] + } + }) + ).to.eq(4) + }) + }) }) diff --git a/packages/server/modules/workspaces/tests/helpers/graphql.ts b/packages/server/modules/workspaces/tests/helpers/graphql.ts index 061758a17..67ec90ef5 100644 --- a/packages/server/modules/workspaces/tests/helpers/graphql.ts +++ b/packages/server/modules/workspaces/tests/helpers/graphql.ts @@ -363,6 +363,28 @@ export const getWorkspaceWithJoinRequestsQuery = gql` ${basicWorkspaceFragment} ` +export const getWorkspaceWithSubscriptionQuery = gql` + query GetWorkspaceWithSubscription($workspaceId: String!) { + workspace(id: $workspaceId) { + ...BasicWorkspace + subscription { + createdAt + updatedAt + currentBillingCycleEnd + billingInterval + seats { + guest + plan + assigned + totalCount + viewersCount + } + } + } + } + ${basicWorkspaceFragment} +` + export const updateWorkspaceProjectRoleMutation = gql` mutation UpdateWorkspaceProjectRole($input: ProjectUpdateRoleInput!) { workspaceMutations { diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index f93621555..008b43a08 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4859,8 +4859,16 @@ export type WorkspaceSubscription = { export type WorkspaceSubscriptionSeats = { __typename?: 'WorkspaceSubscriptionSeats'; + /** Number assigned seats in the current billing cycle */ + assigned: Scalars['Int']['output']; + /** @deprecated Field no longer supported */ guest: Scalars['Int']['output']; + /** @deprecated Field no longer supported */ plan: Scalars['Int']['output']; + /** Total number of seats purchased and available in the current subscription cycle */ + totalCount: Scalars['Int']['output']; + /** Number of viewer seats currently assigned in the workspace */ + viewersCount: Scalars['Int']['output']; }; export type WorkspaceTeamFilter = { @@ -5180,6 +5188,13 @@ export type GetWorkspaceWithJoinRequestsQueryVariables = Exact<{ export type GetWorkspaceWithJoinRequestsQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: string, createdAt: string, role?: string | null, readOnly: boolean, adminWorkspacesJoinRequests?: { __typename?: 'WorkspaceJoinRequestCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'WorkspaceJoinRequest', status: WorkspaceJoinRequestStatus, createdAt: string, user: { __typename?: 'LimitedUser', id: string, name: string }, workspace: { __typename?: 'Workspace', id: string, name: string } }> } | null } }; +export type GetWorkspaceWithSubscriptionQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + + +export type GetWorkspaceWithSubscriptionQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: string, createdAt: string, role?: string | null, readOnly: boolean, subscription?: { __typename?: 'WorkspaceSubscription', createdAt: string, updatedAt: string, currentBillingCycleEnd: string, billingInterval: BillingInterval, seats: { __typename?: 'WorkspaceSubscriptionSeats', guest: number, plan: number, assigned: number, totalCount: number, viewersCount: number } } | null } }; + export type UpdateWorkspaceProjectRoleMutationVariables = Exact<{ input: ProjectUpdateRoleInput; }>; @@ -6020,6 +6035,7 @@ export const OnWorkspaceUpdatedDocument = {"kind":"Document","definitions":[{"ki export const DismissWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"dismissWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceDismissInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dismiss"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode; export const RequestToJoinWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"requestToJoinWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceRequestToJoinInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"requestToJoin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode; 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; +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; 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; 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; export const CreateStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 2f775cc4b..4728913e6 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -492,7 +492,9 @@ const getStream = () => { // REGIONS // https://app.speckle.systems/projects/16ce7b208c/models/1c14e37363@0614bb2957 - // SUPER slow tree build time + // 'https://app.speckle.systems/projects/7591c56179/models/82b94108a3' + + // SUPER slow tree build time (LARGE N-GONS TRIANGULATION) // 'https://app.speckle.systems/projects/0edb6ef628/models/ff3d8480bc@cd83d90a2c' ) } diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 0504b3940..c15667629 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -38,6 +38,7 @@ import { computeOrthographicSize } from '../CameraController.js' import { ObjectLayers } from '../../../IViewer.js' import SpeckleBasicMaterial from '../../materials/SpeckleBasicMaterial.js' import SpeckleRenderer from '../../SpeckleRenderer.js' +import { ExtendedMeshIntersection } from '../../objects/SpeckleRaycaster.js' /** * @param {Number} value @@ -1076,6 +1077,12 @@ export class SmoothOrbitControls extends SpeckleControls { this.orbitSphere.visible = false } + /** By default hidden objects are ignored when picking for orbit around cursor */ + protected filterOrbitToCursorHits(hit: ExtendedMeshIntersection) { + const material = this.renderer.getMaterial(hit.batchObject.renderView) + return material?.visible ?? false + } + protected onPointerDown = (event: PointerEvent) => { if (this._options.orbitAroundCursor) { /** Hope this is not slow */ @@ -1083,7 +1090,7 @@ export class SmoothOrbitControls extends SpeckleControls { const x = ((event.clientX - rect.left) / rect.width) * 2 - 1 const y = ((event.clientY - rect.top) / rect.height) * -2 + 1 - const res = this.renderer.intersections.intersect( + let res = this.renderer.intersections.intersect( this.renderer.scene, this._targetCamera as PerspectiveCamera, new Vector2(x, y), @@ -1091,7 +1098,9 @@ export class SmoothOrbitControls extends SpeckleControls { true, this.renderer.clippingVolume ) - if (res && res.length) { + res = res?.filter(this.filterOrbitToCursorHits.bind(this)) ?? [] + + if (res.length) { this.pivotPoint.copy(res[0].point) this.usePivotal = true this.orbitSphere.visible = this._options.showOrbitPoint