diff --git a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql index 79ed58eba..a5e0a7e8b 100644 --- a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql +++ b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql @@ -393,6 +393,10 @@ type LimitedWorkspace { Workspace members visible to people with verified email domain """ team(cursor: String, limit: Int! = 25): LimitedWorkspaceCollaboratorCollection + """ + Workspace admins ordered by join date + """ + adminTeam: [LimitedWorkspaceCollaborator!]! } type LimitedWorkspaceCollaboratorCollection { diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index 67be207ab..0b809f341 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -72,6 +72,7 @@ generates: WorkspaceBillingMutations: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceBillingMutationsGraphQLReturn' PendingWorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#PendingWorkspaceCollaboratorGraphQLReturn' WorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceCollaboratorGraphQLReturn' + LimitedWorkspace: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceGraphQLReturn' LimitedWorkspaceCollaborator: '@/modules/workspacesCore/helpers/graphTypes#LimitedWorkspaceCollaboratorGraphQLReturn' WorkspaceSubscriptionSeats: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionSeatsGraphQLReturn' WorkspaceJoinRequest: '@/modules/workspacesCore/helpers/graphTypes#WorkspaceJoinRequestGraphQLReturn' diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 3be59d664..30dee1b27 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -5,7 +5,7 @@ import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn, Commen import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes'; import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types'; import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationPermissionChecksGraphQLReturn, 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, LimitedWorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectMoveToWorkspaceDryRunGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'; +import { WorkspaceGraphQLReturn, WorkspaceSsoGraphQLReturn, WorkspaceMutationsGraphQLReturn, WorkspaceJoinRequestMutationsGraphQLReturn, WorkspaceInviteMutationsGraphQLReturn, WorkspaceProjectMutationsGraphQLReturn, PendingWorkspaceCollaboratorGraphQLReturn, WorkspaceCollaboratorGraphQLReturn, LimitedWorkspaceGraphQLReturn, LimitedWorkspaceCollaboratorGraphQLReturn, WorkspaceJoinRequestGraphQLReturn, LimitedWorkspaceJoinRequestGraphQLReturn, ProjectMoveToWorkspaceDryRunGraphQLReturn, ProjectRoleGraphQLReturn, WorkspacePermissionChecksGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'; import { WorkspacePlanGraphQLReturn, WorkspacePlanUsageGraphQLReturn, PriceGraphQLReturn } from '@/modules/gatekeeperCore/helpers/graphTypes'; import { WorkspaceBillingMutationsGraphQLReturn, WorkspaceSubscriptionSeatsGraphQLReturn, WorkspaceSubscriptionGraphQLReturn } from '@/modules/gatekeeper/helpers/graphTypes'; import { WebhookGraphQLReturn } from '@/modules/webhooks/helpers/graphTypes'; @@ -1220,6 +1220,8 @@ export type LimitedUserWorkspaceRoleArgs = { /** Workspace metadata visible to non-workspace members. */ export type LimitedWorkspace = { __typename?: 'LimitedWorkspace'; + /** Workspace admins ordered by join date */ + adminTeam: Array; /** Workspace description */ description?: Maybe; /** Workspace id */ @@ -5344,7 +5346,7 @@ export type ResolversTypes = { JoinWorkspaceInput: JoinWorkspaceInput; LegacyCommentViewerData: ResolverTypeWrapper; LimitedUser: ResolverTypeWrapper; - LimitedWorkspace: ResolverTypeWrapper & { team?: Maybe }>; + LimitedWorkspace: ResolverTypeWrapper; LimitedWorkspaceCollaborator: ResolverTypeWrapper; LimitedWorkspaceCollaboratorCollection: ResolverTypeWrapper & { items: Array }>; LimitedWorkspaceJoinRequest: ResolverTypeWrapper; @@ -5680,7 +5682,7 @@ export type ResolversParentTypes = { JoinWorkspaceInput: JoinWorkspaceInput; LegacyCommentViewerData: LegacyCommentViewerData; LimitedUser: LimitedUserGraphQLReturn; - LimitedWorkspace: Omit & { team?: Maybe }; + LimitedWorkspace: LimitedWorkspaceGraphQLReturn; LimitedWorkspaceCollaborator: LimitedWorkspaceCollaboratorGraphQLReturn; LimitedWorkspaceCollaboratorCollection: Omit & { items: Array }; LimitedWorkspaceJoinRequest: LimitedWorkspaceJoinRequestGraphQLReturn; @@ -6420,6 +6422,7 @@ export type LimitedUserResolvers = { + adminTeam?: Resolver, ParentType, ContextType>; description?: Resolver, ParentType, ContextType>; id?: Resolver; logo?: Resolver, ParentType, ContextType>; 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 dac9da741..aa5538047 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -1200,6 +1200,8 @@ export type LimitedUserWorkspaceRoleArgs = { /** Workspace metadata visible to non-workspace members. */ export type LimitedWorkspace = { __typename?: 'LimitedWorkspace'; + /** Workspace admins ordered by join date */ + adminTeam: Array; /** Workspace description */ description?: Maybe; /** Workspace id */ diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 855936608..99562bb7c 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -1986,6 +1986,16 @@ export = FF_WORKSPACES_MODULE_ENABLED cursor: args.cursor ?? undefined }) return team + }, + adminTeam: async (parent) => { + const team = await getWorkspaceCollaboratorsFactory({ db })({ + workspaceId: parent.id, + limit: 100, + filter: { + roles: [Roles.Workspace.Admin] + } + }) + return team } }, ActiveUserMutations: { diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index 8b3e912d3..da04d41f4 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -93,10 +93,22 @@ export const getUserDiscoverableWorkspacesFactory = if (domains.length === 0) { return [] } - return (await tables + + const workspaces = (await tables .workspaces(db) - .select('workspaces.id as id', 'name', 'slug', 'description', 'logo') - .distinctOn('workspaces.id') + .select( + 'workspaces.id as id', + 'name', + 'slug', + 'description', + 'logo', + tables + .workspacesAcl(db) + .select(knex.raw('count(*)::integer')) + .where(DbWorkspaceAcl.col.workspaceId, knex.ref(Workspaces.col.id)) + .as('teamCount') + ) + .distinctOn(['teamCount', 'workspaces.id']) .join('workspace_domains', 'workspace_domains.workspaceId', 'workspaces.id') .leftJoin( tables.workspacesAcl(db).select('*').where({ userId }).as('acl'), @@ -116,10 +128,13 @@ export const getUserDiscoverableWorkspacesFactory = .whereIn('domain', domains) .where('discoverabilityEnabled', true) .where('verified', true) - .where('role', null)) as Pick< + .where('role', null) + .orderBy([{ column: 'teamCount', order: 'desc' }, 'workspaces.id'])) as Pick< Workspace, 'id' | 'name' | 'slug' | 'description' | 'logo' >[] + + return workspaces } const workspaceWithRoleBaseQuery = ({ diff --git a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts index 4b3536ac2..7fad8175e 100644 --- a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts @@ -30,7 +30,8 @@ import { BasicTestWorkspace, assignToWorkspace, buildBasicTestWorkspace, - createTestWorkspace + createTestWorkspace, + createTestWorkspaces } from '@/modules/workspaces/tests/helpers/creation' import { createUserEmailFactory, @@ -931,6 +932,78 @@ describe('Workspace repositories', () => { expect(otherUserWorkspaces.length).to.equal(1) expect(otherUserWorkspaces[0].id).to.equal(workspaceWithExistingRequest.id) }) + + it('should return workspaces in order of team size', async () => { + const userA: BasicTestUser = { + id: '', + name: 'John Speckle', + email: createRandomEmail(), + verified: true + } + const userB: BasicTestUser = { + id: '', + name: 'John Speckle 2', + email: createRandomEmail(), + verified: true + } + const userC: BasicTestUser = { + id: '', + name: 'John Speckle 2 2', + email: createRandomEmail(), + verified: true + } + const userD: BasicTestUser = { + id: '', + name: 'No Workspace User', + email: createRandomEmail(), + verified: true + } + + const workspaceA: BasicTestWorkspace = { + id: '', + ownerId: '', + name: 'Small Workspace', + slug: cryptoRandomString({ length: 9 }), + discoverabilityEnabled: true + } + const workspaceB: BasicTestWorkspace = { + id: '', + ownerId: '', + name: 'Medium Workspace', + slug: cryptoRandomString({ length: 9 }), + discoverabilityEnabled: true + } + const workspaceC: BasicTestWorkspace = { + id: '', + ownerId: '', + name: 'Large Workspace', + slug: cryptoRandomString({ length: 9 }), + discoverabilityEnabled: true + } + + await createTestUsers([userA, userB, userC]) + await createTestWorkspaces([ + [workspaceA, userA, { domain: 'example.org' }], + [workspaceB, userB, { domain: 'example.org' }], + [workspaceC, userC, { domain: 'example.org' }] + ]) + + await Promise.all([ + assignToWorkspace(workspaceB, userA), + assignToWorkspace(workspaceC, userA), + assignToWorkspace(workspaceC, userB) + ]) + + const workspaces = await getUserDiscoverableWorkspaces({ + domains: ['example.org'], + userId: userD.id + }) + + expect(workspaces.length).to.equal(3) + expect(workspaces[0].id).to.equal(workspaceC.id) + expect(workspaces[1].id).to.equal(workspaceB.id) + expect(workspaces[2].id).to.equal(workspaceA.id) + }) }) describe('getWorkspaceDomainsFactory creates a function, that', () => { diff --git a/packages/server/modules/workspacesCore/helpers/graphTypes.ts b/packages/server/modules/workspacesCore/helpers/graphTypes.ts index 330b7fbbd..da362f848 100644 --- a/packages/server/modules/workspacesCore/helpers/graphTypes.ts +++ b/packages/server/modules/workspacesCore/helpers/graphTypes.ts @@ -36,6 +36,11 @@ export type PendingWorkspaceCollaboratorGraphQLReturn = { } export type WorkspaceCollaboratorGraphQLReturn = WorkspaceTeamMember + +export type LimitedWorkspaceGraphQLReturn = Pick< + Workspace, + 'id' | 'name' | 'slug' | 'description' | 'logo' +> export type LimitedWorkspaceCollaboratorGraphQLReturn = WorkspaceTeamMember export type WorkspacePermissionChecksGraphQLReturn = { diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 93f16bdb5..2189fd814 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -1201,6 +1201,8 @@ export type LimitedUserWorkspaceRoleArgs = { /** Workspace metadata visible to non-workspace members. */ export type LimitedWorkspace = { __typename?: 'LimitedWorkspace'; + /** Workspace admins ordered by join date */ + adminTeam: Array; /** Workspace description */ description?: Maybe; /** Workspace id */