diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts
index 229cd32d7..e43b0d4c6 100644
--- a/packages/frontend-2/lib/common/generated/gql/gql.ts
+++ b/packages/frontend-2/lib/common/generated/gql/gql.ts
@@ -94,7 +94,7 @@ const documents = {
"\n fragment ProjectsInviteBanner on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": types.ProjectsInviteBannerFragmentDoc,
"\n fragment ProjectsInviteBanners on User {\n projectInvites {\n ...ProjectsInviteBanner\n }\n }\n": types.ProjectsInviteBannersFragmentDoc,
"\n fragment SettingsDialog_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n }\n": types.SettingsDialog_WorkspaceFragmentDoc,
- "\n fragment SettingsDialog_User on User {\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n": types.SettingsDialog_UserFragmentDoc,
+ "\n fragment SettingsDialog_User on User {\n id\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n": types.SettingsDialog_UserFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n fragment SettingsSharedProjects_Project on Project {\n id\n name\n visibility\n createdAt\n updatedAt\n models {\n totalCount\n }\n versions {\n totalCount\n }\n team {\n id\n user {\n name\n id\n avatar\n }\n }\n }\n": types.SettingsSharedProjects_ProjectFragmentDoc,
"\n fragment SettingsUserEmails_User on User {\n id\n emails {\n ...SettingsUserEmailCards_UserEmail\n }\n }\n": types.SettingsUserEmails_UserFragmentDoc,
@@ -667,7 +667,7 @@ export function graphql(source: "\n fragment SettingsDialog_Workspace on Worksp
/**
* 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 SettingsDialog_User on User {\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsDialog_User on User {\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n"];
+export function graphql(source: "\n fragment SettingsDialog_User on User {\n id\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsDialog_User on User {\n id\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts
index 43998214c..173ed3ee3 100644
--- a/packages/frontend-2/lib/common/generated/gql/graphql.ts
+++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts
@@ -4372,7 +4372,7 @@ export type ProjectsInviteBannersFragment = { __typename?: 'User', projectInvite
export type SettingsDialog_WorkspaceFragment = { __typename?: 'Workspace', id: string, role?: string | null, name: string, logo?: string | null, defaultLogoIndex: number };
-export type SettingsDialog_UserFragment = { __typename?: 'User', workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, role?: string | null, name: string, logo?: string | null, defaultLogoIndex: number }> } };
+export type SettingsDialog_UserFragment = { __typename?: 'User', id: string, workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, role?: string | null, name: string, logo?: string | null, defaultLogoIndex: number }> } };
export type SettingsServerProjects_ProjectCollectionFragment = { __typename?: 'ProjectCollection', totalCount: number, items: Array<{ __typename?: 'Project', id: string, name: string, visibility: ProjectVisibility, createdAt: string, updatedAt: string, models: { __typename?: 'ModelCollection', totalCount: number }, versions: { __typename?: 'VersionCollection', totalCount: number }, team: Array<{ __typename?: 'ProjectCollaborator', id: string, user: { __typename?: 'LimitedUser', name: string, id: string, avatar?: string | null } }> }> };
@@ -5348,7 +5348,7 @@ export type SettingsLeaveWorkspaceMutation = { __typename?: 'Mutation', workspac
export type SettingsSidebarQueryVariables = Exact<{ [key: string]: never; }>;
-export type SettingsSidebarQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, role?: string | null, name: string, logo?: string | null, defaultLogoIndex: number }> } } | null };
+export type SettingsSidebarQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, role?: string | null, name: string, logo?: string | null, defaultLogoIndex: number }> } } | null };
export type SettingsWorkspaceGeneralQueryVariables = Exact<{
id: Scalars['String']['input'];
@@ -5767,7 +5767,7 @@ export const WorkspaceInviteBanner_PendingWorkspaceCollaboratorFragmentDoc = {"k
export const WorkspaceInviteBanners_UserFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteBanners_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"discoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceInviteBanner_PendingWorkspaceCollaborator"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseWorkspaceInviteManager_PendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DiscoverableWorkspace"}},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteBanner_PendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseWorkspaceInviteManager_PendingWorkspaceCollaborator"}}]}}]} as unknown as DocumentNode
;
export const ProjectsDashboardHeaderWorkspaces_UserFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsDashboardHeaderWorkspaces_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceInviteBanners_User"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DiscoverableWorkspace"}},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseWorkspaceInviteManager_PendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteBanner_PendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseWorkspaceInviteManager_PendingWorkspaceCollaborator"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteBanners_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"discoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceInviteBanner_PendingWorkspaceCollaborator"}}]}}]}}]} as unknown as DocumentNode;
export const SettingsDialog_WorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}}]} as unknown as DocumentNode;
-export const SettingsDialog_UserFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsDialog_Workspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode;
+export const SettingsDialog_UserFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsDialog_Workspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode;
export const SettingsSharedProjects_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsSharedProjects_Project"},"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":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]} as unknown as DocumentNode;
export const SettingsServerProjects_ProjectCollectionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsServerProjects_ProjectCollection"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCollection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsSharedProjects_Project"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsSharedProjects_Project"},"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":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]} as unknown as DocumentNode;
export const SettingsUserEmailCards_UserEmailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsUserEmailCards_UserEmail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserEmail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"primary"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]} as unknown as DocumentNode;
@@ -5963,7 +5963,7 @@ export const SettingsCancelWorkspaceInviteDocument = {"kind":"Document","definit
export const AddWorkspaceDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddWorkspaceDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AddDomainToWorkspaceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addDomain"},"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":"SettingsWorkspacesSecurity_Workspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceDomain"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesSecurity_Workspace"},"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":"domains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain"}}]}},{"kind":"Field","name":{"kind":"Name","value":"domainBasedMembershipProtectionEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode;
export const DeleteWorkspaceDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteWorkspaceDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceDomainDeleteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteDomain"},"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":"SettingsWorkspacesSecurityDomainRemoveDialog_Workspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceDomain"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesSecurityDomainRemoveDialog_Workspace"},"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":"domains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain"}}]}}]}}]} as unknown as DocumentNode;
export const SettingsLeaveWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SettingsLeaveWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"leaveId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"leave"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"leaveId"}}}]}]}}]}}]} as unknown as DocumentNode;
-export const SettingsSidebarDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SettingsSidebar"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsDialog_User"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsDialog_Workspace"}}]}}]}}]}}]} as unknown as DocumentNode;
+export const SettingsSidebarDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SettingsSidebar"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsDialog_User"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceAvatar_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsDialog_User"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsDialog_Workspace"}}]}}]}}]}}]} as unknown as DocumentNode;
export const SettingsWorkspaceGeneralDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SettingsWorkspaceGeneral"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"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":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesGeneral_Workspace"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesGeneralEditAvatar_Workspace"},"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"defaultLogoIndex"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspaceGeneralDeleteDialog_Workspace"},"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":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesGeneral_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesGeneralEditAvatar_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspaceGeneralDeleteDialog_Workspace"}},{"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":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode;
export const SettingsWorkspaceBillingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SettingsWorkspaceBilling"},"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":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesBilling_Workspace"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BillingSummary_WorkspaceCost"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCost"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cost"}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}},{"kind":"Field","name":{"kind":"Name","value":"discount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"subTotal"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesBilling_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"billing"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cost"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subTotal"}},{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"BillingSummary_WorkspaceCost"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versionsCount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"current"}},{"kind":"Field","name":{"kind":"Name","value":"max"}}]}}]}}]}}]} as unknown as DocumentNode;
export const SettingsWorkspacesMembersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SettingsWorkspacesMembers"},"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":"invitesFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaboratorsFilter"}}}],"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":"SettingsWorkspacesMembers_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersMembersTable_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersGuestsTable_Workspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersInvitesTable_Workspace"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceInviteDialog_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"domainBasedMembershipProtectionEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"domains"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"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":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"invitesFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembersTableHeader_Workspace"},"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":"role"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceInviteDialog_Workspace"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceDomainPolicyCompliant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}]}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembers_Workspace"},"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":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembersMembersTable_Workspace"},"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":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersTableHeader_Workspace"}},{"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":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembersGuestsTable_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersTableHeader_Workspace"}},{"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":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SettingsWorkspacesMembersInvitesTable_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersTableHeader_Workspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"invitesFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator"}}]}}]}}]} as unknown as DocumentNode;
diff --git a/packages/frontend-2/lib/common/helpers/constants.ts b/packages/frontend-2/lib/common/helpers/constants.ts
index e12c7edca..5099e3555 100644
--- a/packages/frontend-2/lib/common/helpers/constants.ts
+++ b/packages/frontend-2/lib/common/helpers/constants.ts
@@ -5,7 +5,8 @@ export enum CookieKeys {
AuthToken = 'authn',
Theme = 'theme',
PostAuthRedirect = 'postAuthRedirect',
- DismissedDiscoverableWorkspaces = 'dismissedDiscoverableWorkspaces'
+ DismissedDiscoverableWorkspaces = 'dismissedDiscoverableWorkspaces',
+ DismissedWorkspaceBanner = 'dismissedWorkspaceBanner'
}
/**
diff --git a/packages/frontend-2/lib/viewer/composables/setup.ts b/packages/frontend-2/lib/viewer/composables/setup.ts
index 654f59533..8877e97eb 100644
--- a/packages/frontend-2/lib/viewer/composables/setup.ts
+++ b/packages/frontend-2/lib/viewer/composables/setup.ts
@@ -766,6 +766,12 @@ function setupResponseResourceData(
})
onViewerLoadedResourcesError((err) => {
+ // Show full page error only if serious error (core data couldn't be loaded)
+ const isWorkingLoad = !!viewerLoadedResourcesResult.value?.project.models.items
+ if (isWorkingLoad) {
+ return
+ }
+
globalError.value = createError({
statusCode: 500,
message: `Viewer loaded resource resolution failed: ${err}`
@@ -841,6 +847,13 @@ function setupResponseResourceData(
const commentThreads = computed(() => commentThreadsMetadata.value?.items || [])
onViewerLoadedThreadsError((err) => {
+ // Show full page error only if serious error (core data couldn't be loaded)
+ const isWorkingLoad =
+ !!viewerLoadedThreadsResult.value?.project.commentThreads.items
+ if (isWorkingLoad) {
+ return
+ }
+
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Comment loading failed',
diff --git a/packages/server/modules/notifications/domain/operations.ts b/packages/server/modules/notifications/domain/operations.ts
new file mode 100644
index 000000000..876526f32
--- /dev/null
+++ b/packages/server/modules/notifications/domain/operations.ts
@@ -0,0 +1,14 @@
+import { NotificationPreferences } from '@/modules/notifications/helpers/types'
+
+export type GetSavedUserNotificationPreferences = (
+ userId: string
+) => Promise
+
+export type SaveUserNotificationPreferences = (
+ userId: string,
+ preferences: NotificationPreferences
+) => Promise
+
+export type GetUserNotificationPreferences = (
+ userId: string
+) => Promise
diff --git a/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts b/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts
index c489ee9a6..f4f4faee7 100644
--- a/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts
+++ b/packages/server/modules/notifications/graph/resolvers/userNotificationPreferences.ts
@@ -1,10 +1,25 @@
+import { db } from '@/db/knex'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import {
- updateNotificationPreferences,
- getUserNotificationPreferences
+ getSavedUserNotificationPreferencesFactory,
+ saveUserNotificationPreferencesFactory
+} from '@/modules/notifications/repositories'
+import {
+ getUserNotificationPreferencesFactory,
+ updateNotificationPreferencesFactory
} from '@/modules/notifications/services/notificationPreferences'
-module.exports = {
+const getUserNotificationPreferences = getUserNotificationPreferencesFactory({
+ getSavedUserNotificationPreferences: getSavedUserNotificationPreferencesFactory({
+ db
+ })
+})
+
+const updateNotificationPreferences = updateNotificationPreferencesFactory({
+ saveUserNotificationPreferences: saveUserNotificationPreferencesFactory({ db })
+})
+
+export = {
User: {
async notificationPreferences(parent) {
const preferences = await getUserNotificationPreferences(parent.id)
@@ -12,12 +27,8 @@ module.exports = {
}
},
Mutation: {
- async userNotificationPreferencesUpdate(
- _parent,
- args,
- context: { userId: string }
- ) {
- await updateNotificationPreferences(context.userId, args.preferences)
+ async userNotificationPreferencesUpdate(_parent, args, context) {
+ await updateNotificationPreferences(context.userId!, args.preferences)
return true
}
}
diff --git a/packages/server/modules/notifications/repositories.ts b/packages/server/modules/notifications/repositories.ts
index 296feac24..fd78ad0d4 100644
--- a/packages/server/modules/notifications/repositories.ts
+++ b/packages/server/modules/notifications/repositories.ts
@@ -1,26 +1,36 @@
import { UserNotificationPreferences } from '@/modules/core/dbSchema'
+import {
+ GetSavedUserNotificationPreferences,
+ SaveUserNotificationPreferences
+} from '@/modules/notifications/domain/operations'
import {
NotificationPreferences,
UserNotificationPreferencesRecord
} from '@/modules/notifications/helpers/types'
+import { Knex } from 'knex'
-export async function getUserNotificationPreferences(
- userId: string
-): Promise {
- const userPreferences =
- await UserNotificationPreferences.knex()
+const tables = {
+ userNotificationPreferences: (db: Knex) =>
+ db(UserNotificationPreferences.name)
+}
+
+export const getSavedUserNotificationPreferencesFactory =
+ (deps: { db: Knex }): GetSavedUserNotificationPreferences =>
+ async (userId: string): Promise => {
+ const userPreferences = await tables
+ .userNotificationPreferences(deps.db)
.where({ userId })
.first()
- return userPreferences?.preferences ?? {}
-}
+ return userPreferences?.preferences ?? {}
+ }
-export async function saveUserNotificationPreferences(
- userId: string,
- preferences: NotificationPreferences
-): Promise {
- await UserNotificationPreferences.knex()
- .insert({ userId, preferences })
- .onConflict('userId')
- .merge()
-}
+export const saveUserNotificationPreferencesFactory =
+ (deps: { db: Knex }): SaveUserNotificationPreferences =>
+ async (userId: string, preferences: NotificationPreferences): Promise => {
+ await tables
+ .userNotificationPreferences(deps.db)
+ .insert({ userId, preferences })
+ .onConflict('userId')
+ .merge()
+ }
diff --git a/packages/server/modules/notifications/services/handlers/activityDigest.ts b/packages/server/modules/notifications/services/handlers/activityDigest.ts
index dba7a5ed2..31f855be8 100644
--- a/packages/server/modules/notifications/services/handlers/activityDigest.ts
+++ b/packages/server/modules/notifications/services/handlers/activityDigest.ts
@@ -10,7 +10,6 @@ import {
} from '@/modules/activitystream/helpers/types'
import { getServerInfo } from '@/modules/core/services/generic'
import { ServerInfo, UserRecord } from '@/modules/core/helpers/types'
-import { getUserNotificationPreferences } from '@/modules/notifications/services/notificationPreferences'
import { sendEmail, SendEmailParams } from '@/modules/emails/services/sending'
import { groupBy } from 'lodash'
import { packageRoot } from '@/bootstrap'
@@ -26,37 +25,44 @@ import {
EmailInput,
renderEmail
} from '@/modules/emails/services/emailRendering'
+import { getUserNotificationPreferencesFactory } from '@/modules/notifications/services/notificationPreferences'
+import { getSavedUserNotificationPreferencesFactory } from '@/modules/notifications/repositories'
+import { db } from '@/db/knex'
+import { GetUserNotificationPreferences } from '@/modules/notifications/domain/operations'
-const handler: NotificationHandler = async (msg) => {
- const {
- targetUserId,
- data: { streamIds, start, end }
- } = msg
- await digestNotificationEmailHandler(targetUserId, streamIds, start, end, sendEmail)
-}
-
-export default handler
-
-const digestNotificationEmailHandler = async (
- userId: string,
- streamIds: string[],
- start: Date,
- end: Date,
- emailSender: (params: SendEmailParams) => Promise
-): Promise => {
- const wantDigests =
- (await (await getUserNotificationPreferences(userId)).activityDigest?.email) !==
- false
- const activitySummary = await createActivitySummary(userId, streamIds, start, end)
- // if there are no activities stop early
- if (!wantDigests || !activitySummary || !activitySummary.streamActivities.length)
- return null
- const serverInfo = await getServerInfo()
- const digest = digestSummaryData(activitySummary, serverInfo)
- if (!digest) return null
- const emailInput = await prepareSummaryEmail(digest, serverInfo)
- return await emailSender(emailInput)
-}
+const digestNotificationEmailHandlerFactory =
+ (
+ deps: {
+ getUserNotificationPreferences: GetUserNotificationPreferences
+ createActivitySummary: typeof createActivitySummary
+ getServerInfo: typeof getServerInfo
+ } & PrepareSummaryEmailDeps
+ ) =>
+ async (
+ userId: string,
+ streamIds: string[],
+ start: Date,
+ end: Date,
+ emailSender: (params: SendEmailParams) => Promise
+ ): Promise => {
+ const wantDigests =
+ (await deps.getUserNotificationPreferences(userId)).activityDigest?.email !==
+ false
+ const activitySummary = await deps.createActivitySummary(
+ userId,
+ streamIds,
+ start,
+ end
+ )
+ // if there are no activities stop early
+ if (!wantDigests || !activitySummary || !activitySummary.streamActivities.length)
+ return null
+ const serverInfo = await deps.getServerInfo()
+ const digest = digestSummaryData(activitySummary, serverInfo)
+ if (!digest) return null
+ const emailInput = await prepareSummaryEmailFactory(deps)(digest, serverInfo)
+ return await emailSender(emailInput)
+ }
/**
* Organize the activity summary into topics.
@@ -361,24 +367,27 @@ const flattenActivities = (
return allActivity
}
-export const prepareSummaryEmail = async (
- digest: Digest,
- serverInfo: ServerInfo
-): Promise => {
- const body = await renderEmailBody(digest, serverInfo)
- const cta = {
- title: 'Check activities',
- url: serverInfo.canonicalUrl
- }
- const subject = 'Speckle weekly digest'
- const { text, html } = await renderEmail(
- { mjml: { bodyStart: body.mjml }, text: { bodyStart: body.text }, cta },
- serverInfo,
- digest.user
- )
- return { to: digest.user.email, subject, text, html }
+type PrepareSummaryEmailDeps = {
+ renderEmail: typeof renderEmail
}
+export const prepareSummaryEmailFactory =
+ (deps: PrepareSummaryEmailDeps) =>
+ async (digest: Digest, serverInfo: ServerInfo): Promise => {
+ const body = await renderEmailBody(digest, serverInfo)
+ const cta = {
+ title: 'Check activities',
+ url: serverInfo.canonicalUrl
+ }
+ const subject = 'Speckle weekly digest'
+ const { text, html } = await deps.renderEmail(
+ { mjml: { bodyStart: body.mjml }, text: { bodyStart: body.text }, cta },
+ serverInfo,
+ digest.user
+ )
+ return { to: digest.user.email, subject, text, html }
+ }
+
export const renderEmailBody = async (
digest: Digest,
serverInfo: ServerInfo
@@ -416,3 +425,25 @@ Here's a summary of what happened in the past week
mjml += mjmlTopics.join('\n')
return { text, mjml }
}
+
+const digestNotificationEmailHandler = digestNotificationEmailHandlerFactory({
+ getUserNotificationPreferences: getUserNotificationPreferencesFactory({
+ getSavedUserNotificationPreferences: getSavedUserNotificationPreferencesFactory({
+ db
+ })
+ }),
+ createActivitySummary,
+ getServerInfo,
+ renderEmail
+})
+
+const handler: NotificationHandler = async (msg) => {
+ const {
+ targetUserId,
+ data: { streamIds, start, end }
+ } = msg
+
+ await digestNotificationEmailHandler(targetUserId, streamIds, start, end, sendEmail)
+}
+
+export default handler
diff --git a/packages/server/modules/notifications/services/handlers/mentionedInComment.ts b/packages/server/modules/notifications/services/handlers/mentionedInComment.ts
index 91bad9f81..5b7f6091e 100644
--- a/packages/server/modules/notifications/services/handlers/mentionedInComment.ts
+++ b/packages/server/modules/notifications/services/handlers/mentionedInComment.ts
@@ -157,45 +157,73 @@ function buildEmailTemplateParams(
/**
* Notification that is triggered when a user is mentioned in a comment
*/
-const handler: NotificationHandler = async (msg) => {
- const {
- targetUserId,
- data: { threadId, authorId, streamId, commentId }
- } = msg
+const mentionedInCommentHandlerFactory =
+ (deps: {
+ getUser: typeof getUser
+ getStream: typeof getStream
+ getComment: typeof getComment
+ getServerInfo: typeof getServerInfo
+ renderEmail: typeof renderEmail
+ sendEmail: typeof sendEmail
+ }): NotificationHandler =>
+ async (msg) => {
+ const {
+ targetUserId,
+ data: { threadId, authorId, streamId, commentId }
+ } = msg
- const isCommentAndThreadTheSame = threadId === commentId
+ const isCommentAndThreadTheSame = threadId === commentId
- const [targetUser, author, stream, threadComment, comment, serverInfo] =
- await Promise.all([
- getUser(targetUserId),
- getUser(authorId),
- getStream({ streamId }),
- getComment({ id: threadId }),
- isCommentAndThreadTheSame ? null : getComment({ id: commentId }),
- getServerInfo()
- ])
+ const [targetUser, author, stream, threadComment, comment, serverInfo] =
+ await Promise.all([
+ deps.getUser(targetUserId),
+ deps.getUser(authorId),
+ deps.getStream({ streamId }),
+ deps.getComment({ id: threadId }),
+ isCommentAndThreadTheSame ? null : deps.getComment({ id: commentId }),
+ deps.getServerInfo()
+ ])
- const mentionComment = isCommentAndThreadTheSame ? threadComment : comment
+ const mentionComment = isCommentAndThreadTheSame ? threadComment : comment
- // Validate message
- const state = validate({
- targetUser,
- author,
- stream,
- threadComment,
- mentionComment,
- msg,
- serverInfo
- })
-
- const templateParams = buildEmailTemplateParams(state)
- const { text, html } = await renderEmail(templateParams, serverInfo, targetUser)
- await sendEmail({
- to: state.targetUser.email,
- text,
- html,
- subject: "You've just been mentioned in a Speckle comment"
+ // Validate message
+ const state = validate({
+ targetUser,
+ author,
+ stream,
+ threadComment,
+ mentionComment,
+ msg,
+ serverInfo
+ })
+
+ const templateParams = buildEmailTemplateParams(state)
+ const { text, html } = await deps.renderEmail(
+ templateParams,
+ serverInfo,
+ targetUser
+ )
+ await deps.sendEmail({
+ to: state.targetUser.email,
+ text,
+ html,
+ subject: "You've just been mentioned in a Speckle comment"
+ })
+ }
+
+/**
+ * Notification that is triggered when a user is mentioned in a comment
+ */
+const handler: NotificationHandler = async (...args) => {
+ const mentionedInCommentHandler = mentionedInCommentHandlerFactory({
+ getUser,
+ getStream,
+ getComment,
+ getServerInfo,
+ renderEmail,
+ sendEmail
})
+ return mentionedInCommentHandler(...args)
}
export default handler
diff --git a/packages/server/modules/notifications/services/handlers/newStreamAccessRequest.ts b/packages/server/modules/notifications/services/handlers/newStreamAccessRequest.ts
index 63c040d33..9daba03ed 100644
--- a/packages/server/modules/notifications/services/handlers/newStreamAccessRequest.ts
+++ b/packages/server/modules/notifications/services/handlers/newStreamAccessRequest.ts
@@ -21,47 +21,59 @@ import {
} from '@/modules/emails/services/emailRendering'
import { getServerInfo } from '@/modules/core/services/generic'
import { db } from '@/db/knex'
+import { GetPendingAccessRequest } from '@/modules/accessrequests/domain/operations'
-async function validateMessage(msg: NewStreamAccessRequestMessage) {
- const {
- targetUserId,
- data: { requestId }
- } = msg
-
- const [request, user] = await Promise.all([
- getPendingAccessRequestFactory({ db })(requestId, AccessRequestType.Stream),
- getUser(targetUserId)
- ])
-
- if (!request)
- throw new NotificationValidationError('Nonexistant stream access request')
- if (!user) throw new NotificationValidationError('Nonexistant user')
-
- const [streamWithRole, requester] = await Promise.all([
- getStream({
- streamId: request.resourceId,
- userId: targetUserId
- }),
- getUser(request.requesterId)
- ])
-
- if (!streamWithRole) throw new NotificationValidationError('Nonexistant stream')
- if (streamWithRole.role !== Roles.Stream.Owner)
- throw new NotificationValidationError(
- 'Only stream owners can receive notifications about stream access requests'
- )
- if (!requester)
- throw new NotificationValidationError('User who made the request no longer exists')
-
- return {
- request,
- stream: streamWithRole,
- targetUser: user,
- requester
- }
+type ValidateMessageDeps = {
+ getPendingAccessRequest: GetPendingAccessRequest
+ getUser: typeof getUser
+ getStream: typeof getStream
}
-type ValidatedMessageState = Awaited>
+const validateMessageFactory =
+ (deps: ValidateMessageDeps) => async (msg: NewStreamAccessRequestMessage) => {
+ const {
+ targetUserId,
+ data: { requestId }
+ } = msg
+
+ const [request, user] = await Promise.all([
+ deps.getPendingAccessRequest(requestId, AccessRequestType.Stream),
+ deps.getUser(targetUserId)
+ ])
+
+ if (!request)
+ throw new NotificationValidationError('Nonexistant stream access request')
+ if (!user) throw new NotificationValidationError('Nonexistant user')
+
+ const [streamWithRole, requester] = await Promise.all([
+ deps.getStream({
+ streamId: request.resourceId,
+ userId: targetUserId
+ }),
+ deps.getUser(request.requesterId)
+ ])
+
+ if (!streamWithRole) throw new NotificationValidationError('Nonexistant stream')
+ if (streamWithRole.role !== Roles.Stream.Owner)
+ throw new NotificationValidationError(
+ 'Only stream owners can receive notifications about stream access requests'
+ )
+ if (!requester)
+ throw new NotificationValidationError(
+ 'User who made the request no longer exists'
+ )
+
+ return {
+ request,
+ stream: streamWithRole,
+ targetUser: user,
+ requester
+ }
+ }
+
+type ValidatedMessageState = Awaited<
+ ReturnType>
+>
function buildEmailTemplateHtml(
state: ValidatedMessageState
@@ -106,22 +118,42 @@ function buildEmailTemplateParams(state: ValidatedMessageState): EmailTemplatePa
}
}
-const handler: NotificationHandler = async (msg) => {
- const state = await validateMessage(msg)
- const htmlTemplateParams = buildEmailTemplateParams(state)
- const serverInfo = await getServerInfo()
- const { html, text } = await renderEmail(
- htmlTemplateParams,
- serverInfo,
- state.targetUser
- )
+const newStreamAccessRequestHandlerFactory =
+ (
+ deps: {
+ getServerInfo: typeof getServerInfo
+ renderEmail: typeof renderEmail
+ sendEmail: typeof sendEmail
+ } & ValidateMessageDeps
+ ): NotificationHandler =>
+ async (msg) => {
+ const state = await validateMessageFactory(deps)(msg)
+ const htmlTemplateParams = buildEmailTemplateParams(state)
+ const serverInfo = await deps.getServerInfo()
+ const { html, text } = await deps.renderEmail(
+ htmlTemplateParams,
+ serverInfo,
+ state.targetUser
+ )
- await sendEmail({
- to: state.targetUser.email,
- text,
- html,
- subject: 'A user requested access to your project'
+ await deps.sendEmail({
+ to: state.targetUser.email,
+ text,
+ html,
+ subject: 'A user requested access to your project'
+ })
+ }
+
+const handler: NotificationHandler = (...args) => {
+ const newStreamAccessRequestHandler = newStreamAccessRequestHandlerFactory({
+ getServerInfo,
+ renderEmail,
+ sendEmail,
+ getUser,
+ getStream,
+ getPendingAccessRequest: getPendingAccessRequestFactory({ db })
})
+ return newStreamAccessRequestHandler(...args)
}
export default handler
diff --git a/packages/server/modules/notifications/services/handlers/streamAccessRequestApproved.ts b/packages/server/modules/notifications/services/handlers/streamAccessRequestApproved.ts
index 9430ab523..e8e1ae5a4 100644
--- a/packages/server/modules/notifications/services/handlers/streamAccessRequestApproved.ts
+++ b/packages/server/modules/notifications/services/handlers/streamAccessRequestApproved.ts
@@ -16,35 +16,43 @@ import {
StreamAccessRequestApprovedMessage
} from '@/modules/notifications/helpers/types'
-async function validateMessage(msg: StreamAccessRequestApprovedMessage) {
- const {
- targetUserId,
- data: {
- request: { resourceId },
- finalizedBy
- }
- } = msg
-
- const [targetUser, finalizer, stream] = await Promise.all([
- getUser(targetUserId),
- getUser(finalizedBy),
- getStream({ streamId: resourceId, userId: targetUserId })
- ])
-
- if (!targetUser)
- throw new NotificationValidationError('Invalid notification target user')
- if (!finalizer)
- throw new NotificationValidationError('Invalid notification finalizer')
- if (!stream) throw new NotificationValidationError('Invalid stream')
- if (!stream.role)
- throw new NotificationValidationError(
- 'User doesnt appear to have a role on the stream'
- )
-
- return { targetUser, finalizer, stream }
+type ValidateMessageDeps = {
+ getUser: typeof getUser
+ getStream: typeof getStream
}
-type ValidatedMessageState = Awaited>
+const validateMessageFactory =
+ (deps: ValidateMessageDeps) => async (msg: StreamAccessRequestApprovedMessage) => {
+ const {
+ targetUserId,
+ data: {
+ request: { resourceId },
+ finalizedBy
+ }
+ } = msg
+
+ const [targetUser, finalizer, stream] = await Promise.all([
+ deps.getUser(targetUserId),
+ deps.getUser(finalizedBy),
+ deps.getStream({ streamId: resourceId, userId: targetUserId })
+ ])
+
+ if (!targetUser)
+ throw new NotificationValidationError('Invalid notification target user')
+ if (!finalizer)
+ throw new NotificationValidationError('Invalid notification finalizer')
+ if (!stream) throw new NotificationValidationError('Invalid stream')
+ if (!stream.role)
+ throw new NotificationValidationError(
+ 'User doesnt appear to have a role on the stream'
+ )
+
+ return { targetUser, finalizer, stream }
+ }
+
+type ValidatedMessageState = Awaited<
+ ReturnType>
+>
function buildEmailTemplateMjml(
state: ValidatedMessageState
@@ -87,24 +95,43 @@ function buildEmailTemplateParams(state: ValidatedMessageState): EmailTemplatePa
}
}
-const handler: NotificationHandler = async (
- msg
-) => {
- const state = await validateMessage(msg)
- const htmlTemplateParams = buildEmailTemplateParams(state)
- const serverInfo = await getServerInfo()
- const { html, text } = await renderEmail(
- htmlTemplateParams,
- serverInfo,
- state.targetUser
- )
+const streamAccessRequestApprovedHandlerFactory =
+ (
+ deps: {
+ getServerInfo: typeof getServerInfo
+ renderEmail: typeof renderEmail
+ sendEmail: typeof sendEmail
+ } & ValidateMessageDeps
+ ): NotificationHandler =>
+ async (msg) => {
+ const state = await validateMessageFactory(deps)(msg)
+ const htmlTemplateParams = buildEmailTemplateParams(state)
+ const serverInfo = await deps.getServerInfo()
+ const { html, text } = await deps.renderEmail(
+ htmlTemplateParams,
+ serverInfo,
+ state.targetUser
+ )
- await sendEmail({
- to: state.targetUser.email,
- text,
- html,
- subject: 'Your project access request has been approved'
+ await deps.sendEmail({
+ to: state.targetUser.email,
+ text,
+ html,
+ subject: 'Your project access request has been approved'
+ })
+ }
+
+const handler: NotificationHandler = async (
+ ...args
+) => {
+ const streamAccessRequestApprovedHandler = streamAccessRequestApprovedHandlerFactory({
+ getServerInfo,
+ renderEmail,
+ sendEmail,
+ getUser,
+ getStream
})
+ return streamAccessRequestApprovedHandler(...args)
}
export default handler
diff --git a/packages/server/modules/notifications/services/notificationPreferences.ts b/packages/server/modules/notifications/services/notificationPreferences.ts
index 07b8dc28f..02d84ca64 100644
--- a/packages/server/modules/notifications/services/notificationPreferences.ts
+++ b/packages/server/modules/notifications/services/notificationPreferences.ts
@@ -1,19 +1,25 @@
-import * as repo from '@/modules/notifications/repositories'
import {
NotificationChannel,
NotificationType,
NotificationPreferences
} from '@/modules/notifications/helpers/types'
import { InvalidArgumentError } from '@/modules/shared/errors'
+import {
+ GetSavedUserNotificationPreferences,
+ GetUserNotificationPreferences,
+ SaveUserNotificationPreferences
+} from '@/modules/notifications/domain/operations'
-export async function getUserNotificationPreferences(
- userId: string
-): Promise {
- const savedPreferences = await repo.getUserNotificationPreferences(userId)
- return addDefaultPreferenceValues(savedPreferences)
-}
+export const getUserNotificationPreferencesFactory =
+ (deps: {
+ getSavedUserNotificationPreferences: GetSavedUserNotificationPreferences
+ }): GetUserNotificationPreferences =>
+ async (userId: string): Promise => {
+ const savedPreferences = await deps.getSavedUserNotificationPreferences(userId)
+ return addDefaultPreferenceValues(savedPreferences)
+ }
-export function addDefaultPreferenceValues(
+function addDefaultPreferenceValues(
preferences: NotificationPreferences
): NotificationPreferences {
const savedPreferences = { ...preferences }
@@ -29,35 +35,34 @@ export function addDefaultPreferenceValues(
return savedPreferences
}
-export async function updateNotificationPreferences(
- userId: string,
- rawPreferences: Record
-): Promise {
- const parsedPreferences: NotificationPreferences = {}
- // lets do some nested attribute copying, to sanitize the input
- for (const key in rawPreferences) {
- if (!Object.values(NotificationType).includes(key as NotificationType))
- throw new InvalidArgumentError(
- `Notification preferences input contains an unknown setting: ${key}`
- )
- const nt = key as NotificationType
- const notificationTypePreferences: Partial> =
- {}
- const notificationTypeSettings = rawPreferences[nt] as Record
- for (const ncKey in notificationTypeSettings) {
- if (!Object.values(NotificationChannel).includes(ncKey as NotificationChannel))
+export const updateNotificationPreferencesFactory =
+ (deps: { saveUserNotificationPreferences: SaveUserNotificationPreferences }) =>
+ async (userId: string, rawPreferences: Record): Promise => {
+ const parsedPreferences: NotificationPreferences = {}
+ // lets do some nested attribute copying, to sanitize the input
+ for (const key in rawPreferences) {
+ if (!Object.values(NotificationType).includes(key as NotificationType))
throw new InvalidArgumentError(
- `Notification preferences input contains an unknown setting: ${ncKey}`
+ `Notification preferences input contains an unknown setting: ${key}`
)
- const nc = ncKey as NotificationChannel
- const preferenceValue = notificationTypeSettings[nc]
- if (typeof preferenceValue !== 'boolean')
- throw new InvalidArgumentError(
- `Notification preferences input contains and invalid value: ${preferenceValue}`
- )
- notificationTypePreferences[nc] = preferenceValue
+ const nt = key as NotificationType
+ const notificationTypePreferences: Partial> =
+ {}
+ const notificationTypeSettings = rawPreferences[nt] as Record
+ for (const ncKey in notificationTypeSettings) {
+ if (!Object.values(NotificationChannel).includes(ncKey as NotificationChannel))
+ throw new InvalidArgumentError(
+ `Notification preferences input contains an unknown setting: ${ncKey}`
+ )
+ const nc = ncKey as NotificationChannel
+ const preferenceValue = notificationTypeSettings[nc]
+ if (typeof preferenceValue !== 'boolean')
+ throw new InvalidArgumentError(
+ `Notification preferences input contains and invalid value: ${preferenceValue}`
+ )
+ notificationTypePreferences[nc] = preferenceValue
+ }
+ parsedPreferences[nt] = notificationTypePreferences
}
- parsedPreferences[nt] = notificationTypePreferences
+ return await deps.saveUserNotificationPreferences(userId, parsedPreferences)
}
- return await repo.saveUserNotificationPreferences(userId, parsedPreferences)
-}
diff --git a/packages/server/modules/notifications/tests/activityDigest.spec.ts b/packages/server/modules/notifications/tests/activityDigest.spec.ts
index a4a89259f..b773c28f1 100644
--- a/packages/server/modules/notifications/tests/activityDigest.spec.ts
+++ b/packages/server/modules/notifications/tests/activityDigest.spec.ts
@@ -9,6 +9,7 @@ import {
StreamActivitySummary
} from '@/modules/activitystream/services/summary'
import { ServerInfo, UserRecord } from '@/modules/core/helpers/types'
+import { renderEmail } from '@/modules/emails/services/emailRendering'
import {
digestMostActiveStream,
mostActiveComment,
@@ -19,11 +20,15 @@ import {
digestActiveStreams,
closingOverview,
Digest,
- prepareSummaryEmail
+ prepareSummaryEmailFactory
} from '@/modules/notifications/services/handlers/activityDigest'
import { expect } from 'chai'
import { range } from 'lodash'
+const prepareSummaryEmail = prepareSummaryEmailFactory({
+ renderEmail
+})
+
describe('Activity digest notifications @notifications', () => {
const user: UserRecord = {
id: 'foobar',
diff --git a/packages/server/modules/notifications/tests/notificationsPreferences.spec.ts b/packages/server/modules/notifications/tests/notificationsPreferences.spec.ts
index 913d39544..559291328 100644
--- a/packages/server/modules/notifications/tests/notificationsPreferences.spec.ts
+++ b/packages/server/modules/notifications/tests/notificationsPreferences.spec.ts
@@ -1,14 +1,31 @@
import { truncateTables } from '@/test/hooks'
import { UserNotificationPreferences, Users } from '@/modules/core/dbSchema'
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
-import * as repo from '@/modules/notifications/repositories'
-import * as services from '@/modules/notifications/services/notificationPreferences'
import { expect } from 'chai'
import {
NotificationType,
NotificationChannel
} from '@/modules/notifications/helpers/types'
import { BaseError } from '@/modules/shared/errors'
+import {
+ getUserNotificationPreferencesFactory,
+ updateNotificationPreferencesFactory
+} from '@/modules/notifications/services/notificationPreferences'
+import {
+ getSavedUserNotificationPreferencesFactory,
+ saveUserNotificationPreferencesFactory
+} from '@/modules/notifications/repositories'
+import { db } from '@/db/knex'
+
+const getSavedUserNotificationPreferences = getSavedUserNotificationPreferencesFactory({
+ db
+})
+const getUserNotificationPreferences = getUserNotificationPreferencesFactory({
+ getSavedUserNotificationPreferences
+})
+const updateNotificationPreferences = updateNotificationPreferencesFactory({
+ saveUserNotificationPreferences: saveUserNotificationPreferencesFactory({ db })
+})
const cleanup = async () => {
await truncateTables([Users.name, UserNotificationPreferences.name])
@@ -28,10 +45,10 @@ describe('User notification preferences @notifications', () => {
describe('services', () => {
it('gets default preferences if none saved', async () => {
- const savedPreferences = await repo.getUserNotificationPreferences(userA.id)
+ const savedPreferences = await getSavedUserNotificationPreferences(userA.id)
expect(savedPreferences).to.deep.equal({})
expect(savedPreferences).to.be.empty
- const preferences = await services.getUserNotificationPreferences(userA.id)
+ const preferences = await getUserNotificationPreferences(userA.id)
expect(preferences).to.not.be.empty
for (const val of Object.values(preferences)) {
for (const setting of Object.values(val)) {
@@ -40,16 +57,16 @@ describe('User notification preferences @notifications', () => {
}
})
it('store notification settings', async () => {
- await services.updateNotificationPreferences(userA.id, {
+ await updateNotificationPreferences(userA.id, {
activityDigest: { email: false }
})
- let preferences = await services.getUserNotificationPreferences(userA.id)
+ let preferences = await getUserNotificationPreferences(userA.id)
expect(preferences).to.not.be.empty
expect(preferences.activityDigest?.email).to.be.false
- await services.updateNotificationPreferences(userA.id, {
+ await updateNotificationPreferences(userA.id, {
activityDigest: { email: true }
})
- preferences = await services.getUserNotificationPreferences(userA.id)
+ preferences = await getUserNotificationPreferences(userA.id)
expect(preferences.activityDigest?.email).to.be.true
})
it("doesn't store invalid preference keys", async () => {
@@ -72,7 +89,7 @@ describe('User notification preferences @notifications', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
preferences[nt][nc] = value
- await services.updateNotificationPreferences(userA.id, preferences)
+ await updateNotificationPreferences(userA.id, preferences)
} catch (err) {
expect(err instanceof BaseError)
const error = err as BaseError
diff --git a/packages/server/modules/workspaces/events/eventListener.ts b/packages/server/modules/workspaces/events/eventListener.ts
index 50707fe69..3aacb0256 100644
--- a/packages/server/modules/workspaces/events/eventListener.ts
+++ b/packages/server/modules/workspaces/events/eventListener.ts
@@ -3,7 +3,11 @@ import {
ProjectEvents,
ProjectEventsPayloads
} from '@/modules/core/events/projectsEmitter'
-import { getStream } from '@/modules/core/repositories/streams'
+import {
+ deleteProjectRoleFactory,
+ getStream,
+ upsertProjectRoleFactory
+} from '@/modules/core/repositories/streams'
import {
GetWorkspaceRoles,
GetWorkspaceRoleToDefaultProjectRoleMapping,
@@ -17,13 +21,27 @@ import {
isProjectResourceTarget,
resolveTarget
} from '@/modules/serverinvites/helpers/core'
-import { logger } from '@/logging/logging'
+import { logger, moduleLogger } from '@/logging/logging'
import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
import { Roles, WorkspaceRoles } from '@speckle/shared'
-import { UpsertProjectRole } from '@/modules/core/domain/projects/operations'
+import {
+ DeleteProjectRole,
+ UpsertProjectRole
+} from '@/modules/core/domain/projects/operations'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
+import { Knex } from 'knex'
+import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
+import {
+ getWorkspaceRolesFactory,
+ getWorkspaceWithDomainsFactory,
+ upsertWorkspaceRoleFactory
+} from '@/modules/workspaces/repositories/workspaces'
+import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
+import { getStreams } from '@/modules/core/services/streams'
+import { withTransaction } from '@/modules/shared/helpers/dbHelper'
+import { findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
export const onProjectCreatedFactory =
({
@@ -89,7 +107,7 @@ export const onInviteFinalizedFactory =
})
if (!project || !project.role) {
deps.logger.warn(
- `When handling accepted invite - project not found or useris not a collaborator`,
+ `When handling accepted invite - project not found or user is not a collaborator`,
{ invite, project: { id: project?.id, role: project?.role } }
)
return
@@ -109,39 +127,77 @@ export const onInviteFinalizedFactory =
})
}
-export const onWorkspaceJoinedFactory =
+export const onWorkspaceRoleDeletedFactory =
+ ({
+ queryAllWorkspaceProjects,
+ deleteProjectRole
+ }: {
+ queryAllWorkspaceProjects: QueryAllWorkspaceProjects
+ deleteProjectRole: DeleteProjectRole
+ }) =>
+ async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
+ // Delete roles for all workspace projects
+ for await (const projectsPage of queryAllWorkspaceProjects({
+ workspaceId
+ })) {
+ await Promise.all(
+ projectsPage.map(({ id: projectId }) =>
+ deleteProjectRole({ projectId, userId })
+ )
+ )
+ }
+ }
+
+export const onWorkspaceRoleUpdatedFactory =
({
getDefaultWorkspaceProjectRoleMapping,
queryAllWorkspaceProjects,
+ deleteProjectRole,
upsertProjectRole
}: {
getDefaultWorkspaceProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
+ deleteProjectRole: DeleteProjectRole
upsertProjectRole: UpsertProjectRole
}) =>
async ({
userId,
role,
- workspaceId
+ workspaceId,
+ flags
}: {
userId: string
role: WorkspaceRoles
workspaceId: string
+ flags?: {
+ skipProjectRoleUpdatesFor: string[]
+ }
}) => {
- const defaultRoleMapping = await getDefaultWorkspaceProjectRoleMapping({
+ const defaultProjectRoleMapping = await getDefaultWorkspaceProjectRoleMapping({
workspaceId
})
- const maybeProjectRole = defaultRoleMapping[role]
- if (!maybeProjectRole) return
+ const nextProjectRole = defaultProjectRoleMapping[role]
- for await (const projects of queryAllWorkspaceProjects({ workspaceId })) {
+ for await (const projectsPage of queryAllWorkspaceProjects({ workspaceId })) {
await Promise.all(
- projects.map(async (project) => {
+ projectsPage.map(async ({ id: projectId }) => {
+ if (flags?.skipProjectRoleUpdatesFor.includes(projectId)) {
+ // Skip assignment (used during invite flow)
+ // TODO: Can we refactor this special case away?
+ return
+ }
+
+ if (!nextProjectRole) {
+ // User is being demoted to a workspace role without project access
+ await deleteProjectRole({ projectId, userId })
+ return
+ }
+
await upsertProjectRole({
- projectId: project.id,
+ projectId,
userId,
- role: maybeProjectRole
+ role: nextProjectRole
})
})
)
@@ -149,25 +205,50 @@ export const onWorkspaceJoinedFactory =
}
export const initializeEventListenersFactory =
- ({
- onProjectCreated,
- onInviteFinalized,
- onWorkspaceJoined
- }: {
- onProjectCreated: ReturnType
- onInviteFinalized: ReturnType
- onWorkspaceJoined: ReturnType
- }) =>
+ ({ db }: { db: Knex }) =>
() => {
const eventBus = getEventBus()
const quitCbs = [
- ProjectsEmitter.listen(ProjectEvents.Created, onProjectCreated),
- eventBus.listen(ServerInvitesEvents.Finalized, ({ payload }) =>
- onInviteFinalized(payload)
- ),
- eventBus.listen(WorkspaceEvents.JoinedFromDiscovery, ({ payload }) =>
- onWorkspaceJoined(payload)
- )
+ ProjectsEmitter.listen(ProjectEvents.Created, async (payload) => {
+ const onProjectCreated = onProjectCreatedFactory({
+ getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
+ upsertProjectRole: upsertProjectRoleFactory({ db }),
+ getWorkspaceRoles: getWorkspaceRolesFactory({ db })
+ })
+ await onProjectCreated(payload)
+ }),
+ eventBus.listen(ServerInvitesEvents.Finalized, async ({ payload }) => {
+ const onInviteFinalized = onInviteFinalizedFactory({
+ getStream,
+ logger: moduleLogger,
+ updateWorkspaceRole: updateWorkspaceRoleFactory({
+ getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
+ findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
+ getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
+ upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
+ emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
+ })
+ })
+ await onInviteFinalized(payload)
+ }),
+ eventBus.listen(WorkspaceEvents.RoleDeleted, async ({ payload }) => {
+ const trx = await db.transaction()
+ const onWorkspaceRoleDeleted = onWorkspaceRoleDeletedFactory({
+ queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
+ deleteProjectRole: deleteProjectRoleFactory({ db: trx })
+ })
+ await withTransaction(onWorkspaceRoleDeleted(payload), trx)
+ }),
+ eventBus.listen(WorkspaceEvents.RoleUpdated, async ({ payload }) => {
+ const trx = await db.transaction()
+ const onWorkspaceRoleUpdated = onWorkspaceRoleUpdatedFactory({
+ getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
+ queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
+ deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
+ upsertProjectRole: upsertProjectRoleFactory({ db: trx })
+ })
+ await withTransaction(onWorkspaceRoleUpdated(payload), trx)
+ })
]
return () => quitCbs.forEach((quit) => quit())
diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts
index 9982c936e..1404c247f 100644
--- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts
+++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts
@@ -5,8 +5,6 @@ import {
getStream,
getUserStreams,
getUserStreamsCount,
- upsertProjectRoleFactory,
- deleteProjectRoleFactory,
getRolesByUserIdFactory
} from '@/modules/core/repositories/streams'
import { getUser, getUsers } from '@/modules/core/repositories/users'
@@ -123,7 +121,6 @@ import {
isUserWorkspaceDomainPolicyCompliantFactory
} from '@/modules/workspaces/services/domains'
import { getServerInfo } from '@/modules/core/services/generic'
-import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
import { updateStreamRoleAndNotify } from '@/modules/core/services/streams/management'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
@@ -355,10 +352,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
- deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
- getStreams
- }),
emitWorkspaceEvent: getEventBus().emit
})
@@ -377,13 +370,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
db: trx
}),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
- getDefaultWorkspaceProjectRoleMapping:
- mapWorkspaceRoleToInitialProjectRole,
- upsertProjectRole: upsertProjectRoleFactory({ db: trx }),
- deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
- getStreams
- }),
emitWorkspaceEvent: getEventBus().emit
})
@@ -466,8 +452,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
- deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
emitWorkspaceEvent: getEventBus().emit
})
@@ -581,13 +565,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
- upsertProjectRole: upsertProjectRoleFactory({ db }),
- getDefaultWorkspaceProjectRoleMapping:
- mapWorkspaceRoleToInitialProjectRole,
- deleteProjectRole: deleteProjectRoleFactory({ db }),
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({
- getStreams
- }),
emitWorkspaceEvent: getEventBus().emit
})
}),
diff --git a/packages/server/modules/workspaces/index.ts b/packages/server/modules/workspaces/index.ts
index 43fb1e1ae..8a361f187 100644
--- a/packages/server/modules/workspaces/index.ts
+++ b/packages/server/modules/workspaces/index.ts
@@ -6,29 +6,8 @@ import { Optional, SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { workspaceRoles } from '@/modules/workspaces/roles'
import { workspaceScopes } from '@/modules/workspaces/scopes'
import { registerOrUpdateRole } from '@/modules/shared/repositories/roles'
-import {
- initializeEventListenersFactory,
- onInviteFinalizedFactory,
- onProjectCreatedFactory,
- onWorkspaceJoinedFactory
-} from '@/modules/workspaces/events/eventListener'
-import {
- getWorkspaceRolesFactory,
- getWorkspaceWithDomainsFactory,
- upsertWorkspaceRoleFactory
-} from '@/modules/workspaces/repositories/workspaces'
-import {
- deleteProjectRoleFactory,
- getStream,
- upsertProjectRoleFactory
-} from '@/modules/core/repositories/streams'
-import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management'
-import { getEventBus } from '@/modules/shared/services/eventBus'
-import { getStreams } from '@/modules/core/services/streams'
-import { findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
+import { initializeEventListenersFactory } from '@/modules/workspaces/events/eventListener'
import { validateModuleLicense } from '@/modules/gatekeeper/services/validateLicense'
-import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
-import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
@@ -58,33 +37,7 @@ const workspacesModule: SpeckleModule = {
moduleLogger.info('⚒️ Init workspaces module')
if (isInitial) {
- quitListeners = initializeEventListenersFactory({
- onProjectCreated: onProjectCreatedFactory({
- getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
- upsertProjectRole: upsertProjectRoleFactory({ db }),
- getWorkspaceRoles: getWorkspaceRolesFactory({ db })
- }),
- onWorkspaceJoined: onWorkspaceJoinedFactory({
- getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
- upsertProjectRole: upsertProjectRoleFactory({ db })
- }),
- onInviteFinalized: onInviteFinalizedFactory({
- getStream,
- logger: moduleLogger,
- updateWorkspaceRole: updateWorkspaceRoleFactory({
- getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
- findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
- getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
- upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
- getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
- upsertProjectRole: upsertProjectRoleFactory({ db }),
- deleteProjectRole: deleteProjectRoleFactory({ db }),
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
- emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
- })
- })
- })()
+ quitListeners = initializeEventListenersFactory({ db })()
}
await Promise.all([initScopes(), initRoles()])
},
diff --git a/packages/server/modules/workspaces/services/management.ts b/packages/server/modules/workspaces/services/management.ts
index eea98acb1..289c46902 100644
--- a/packages/server/modules/workspaces/services/management.ts
+++ b/packages/server/modules/workspaces/services/management.ts
@@ -9,7 +9,6 @@ import {
UpsertWorkspaceRole,
GetWorkspaceWithDomains,
GetWorkspaceDomains,
- GetWorkspaceRoleToDefaultProjectRoleMapping,
UpdateWorkspace
} from '@/modules/workspaces/domain/operations'
import {
@@ -52,10 +51,6 @@ import { DeleteAllResourceInvites } from '@/modules/serverinvites/domain/operati
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
import { ProjectInviteResourceType } from '@/modules/serverinvites/domain/constants'
import { chunk, isEmpty, omit } from 'lodash'
-import {
- DeleteProjectRole,
- UpsertProjectRole
-} from '@/modules/core/domain/projects/operations'
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
import { workspaceRoles as workspaceRoleDefinitions } from '@/modules/workspaces/roles'
import { blockedDomains } from '@speckle/shared'
@@ -229,15 +224,11 @@ export const deleteWorkspaceRoleFactory =
({
getWorkspaceRoles,
deleteWorkspaceRole,
- emitWorkspaceEvent,
- queryAllWorkspaceProjects,
- deleteProjectRole
+ emitWorkspaceEvent
}: {
getWorkspaceRoles: GetWorkspaceRoles
deleteWorkspaceRole: DeleteWorkspaceRole
emitWorkspaceEvent: EmitWorkspaceEvent
- queryAllWorkspaceProjects: QueryAllWorkspaceProjects
- deleteProjectRole: DeleteProjectRole
}) =>
async ({
workspaceId,
@@ -255,17 +246,6 @@ export const deleteWorkspaceRoleFactory =
return null
}
- // Delete workspace project roles
- for await (const projectsPage of queryAllWorkspaceProjects({
- workspaceId
- })) {
- await Promise.all(
- projectsPage.map(({ id: projectId }) =>
- deleteProjectRole({ projectId, userId })
- )
- )
- }
-
// Emit deleted role
await emitWorkspaceEvent({
eventName: WorkspaceEvents.RoleDeleted,
@@ -295,20 +275,12 @@ export const updateWorkspaceRoleFactory =
getWorkspaceWithDomains,
findVerifiedEmailsByUserId,
upsertWorkspaceRole,
- upsertProjectRole,
- deleteProjectRole,
- getDefaultWorkspaceProjectRoleMapping,
- queryAllWorkspaceProjects,
emitWorkspaceEvent
}: {
getWorkspaceRoles: GetWorkspaceRoles
getWorkspaceWithDomains: GetWorkspaceWithDomains
findVerifiedEmailsByUserId: FindVerifiedEmailsByUserId
upsertWorkspaceRole: UpsertWorkspaceRole
- upsertProjectRole: UpsertProjectRole
- deleteProjectRole: DeleteProjectRole
- getDefaultWorkspaceProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
- queryAllWorkspaceProjects: QueryAllWorkspaceProjects
emitWorkspaceEvent: EmitWorkspaceEvent
}) =>
async ({
@@ -327,8 +299,16 @@ export const updateWorkspaceRoleFactory =
*/
preventRoleDowngrade?: boolean
}): Promise => {
- // Protect against removing last admin
const workspaceRoles = await getWorkspaceRoles({ workspaceId })
+
+ // Return early if no work required
+ const previousWorkspaceRole = workspaceRoles.find((acl) => acl.userId === userId)
+
+ if (previousWorkspaceRole?.role === nextWorkspaceRole) {
+ return
+ }
+
+ // Protect against removing last admin
if (
isUserLastWorkspaceAdmin(workspaceRoles, userId) &&
nextWorkspaceRole !== Roles.Workspace.Admin
@@ -336,8 +316,6 @@ export const updateWorkspaceRoleFactory =
throw new WorkspaceAdminRequiredError()
}
- const previousWorkspaceRole = workspaceRoles.find((acl) => acl.userId === userId)
-
// prevent role downgrades (used during invite flow)
if (preventRoleDowngrade) {
if (previousWorkspaceRole) {
@@ -369,7 +347,7 @@ export const updateWorkspaceRoleFactory =
}
}
- // Perform upsert
+ // Perform and emit change
await upsertWorkspaceRole({
userId,
workspaceId,
@@ -377,45 +355,17 @@ export const updateWorkspaceRoleFactory =
createdAt: previousWorkspaceRole?.createdAt ?? new Date()
})
- // Emit new role
await emitWorkspaceEvent({
eventName: WorkspaceEvents.RoleUpdated,
- payload: { userId, workspaceId, role: nextWorkspaceRole }
+ payload: {
+ userId,
+ workspaceId,
+ role: nextWorkspaceRole,
+ flags: {
+ skipProjectRoleUpdatesFor: skipProjectRoleUpdatesFor ?? []
+ }
+ }
})
-
- // Update roles for all workspace projects
- const defaultProjectRoleMapping = await getDefaultWorkspaceProjectRoleMapping({
- workspaceId
- })
-
- for await (const projectsPage of queryAllWorkspaceProjects({
- workspaceId
- })) {
- await Promise.all(
- projectsPage.map(({ id: projectId }) => {
- // skip assigning project role implied by workspace role (used during invite flow)
- if (skipProjectRoleUpdatesFor?.includes(projectId)) {
- return
- }
-
- // no change required
- if (previousWorkspaceRole?.role === nextWorkspaceRole) return
-
- const nextProjectRole = defaultProjectRoleMapping[nextWorkspaceRole]
-
- // user is being removed from workspace or demoted to workspace guest
- if (!nextWorkspaceRole || !nextProjectRole)
- return deleteProjectRole({ projectId, userId })
-
- // user is being granted a workspace role with new role for given project
- return upsertProjectRole({
- projectId,
- userId,
- role: nextProjectRole
- })
- })
- )
- }
}
export const addDomainToWorkspaceFactory =
diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts
index d8521362c..9ad78eb93 100644
--- a/packages/server/modules/workspaces/tests/helpers/creation.ts
+++ b/packages/server/modules/workspaces/tests/helpers/creation.ts
@@ -1,21 +1,15 @@
import { db } from '@/db/knex'
-import {
- deleteProjectRoleFactory,
- getStream,
- upsertProjectRoleFactory
-} from '@/modules/core/repositories/streams'
+import { getStream } from '@/modules/core/repositories/streams'
import {
findEmailsByUserIdFactory,
findVerifiedEmailsByUserIdFactory
} from '@/modules/core/repositories/userEmails'
-import { getStreams } from '@/modules/core/services/streams'
import {
findUserByTargetFactory,
insertInviteAndDeleteOldFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import { getEventBus } from '@/modules/shared/services/eventBus'
-import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
import {
getWorkspaceRolesFactory,
upsertWorkspaceFactory,
@@ -38,7 +32,6 @@ import {
updateWorkspaceFactory,
addDomainToWorkspaceFactory
} from '@/modules/workspaces/services/management'
-import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
import { BasicTestUser } from '@/test/authHelper'
import { CreateWorkspaceInviteMutationVariables } from '@/test/graphql/generated/graphql'
import { MaybeNullOrUndefined, Roles, WorkspaceRoles } from '@speckle/shared'
@@ -138,10 +131,6 @@ export const assignToWorkspace = async (
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
- getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
- upsertProjectRole: upsertProjectRoleFactory({ db }),
- deleteProjectRole: deleteProjectRoleFactory({ db }),
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
})
@@ -159,8 +148,6 @@ export const unassignFromWorkspace = async (
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
deleteWorkspaceRole: dbDeleteWorkspaceRoleFactory({ db }),
- deleteProjectRole: deleteProjectRoleFactory({ db }),
- queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
})
diff --git a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts
index c25cc9c10..346e11f4d 100644
--- a/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts
+++ b/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts
@@ -565,11 +565,9 @@ describe('Workspaces GQL CRUD', () => {
// first 10 users
await createTestUsers(freeGuests)
- await Promise.all(
- freeGuests.map((guest) =>
- assignToWorkspace(workspace, guest, Roles.Workspace.Guest)
- )
- )
+ for (const guest of freeGuests) {
+ await assignToWorkspace(workspace, guest, Roles.Workspace.Guest)
+ }
await Promise.all([
createTestUser(member),
diff --git a/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts b/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts
index c8282a9a4..6a3d4f182 100644
--- a/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts
+++ b/packages/server/modules/workspaces/tests/unit/events/eventListener.spec.ts
@@ -4,7 +4,7 @@ import { Roles, StreamRoles } from '@speckle/shared'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import {
onProjectCreatedFactory,
- onWorkspaceJoinedFactory
+ onWorkspaceRoleUpdatedFactory
} from '@/modules/workspaces/events/eventListener'
import { expect } from 'chai'
import { mapWorkspaceRoleToInitialProjectRole } from '@/modules/workspaces/domain/logic'
@@ -61,16 +61,22 @@ describe('Event handlers', () => {
expect(projectRoles.length).to.equal(2)
})
})
- describe('onWorkspaceJoinedFactory creates a function, that', () => {
+ describe('onWorkspaceRoleUpdatedFactory creates a function, that', () => {
it('assigns no project roles if the role mapping returns null', async () => {
- await onWorkspaceJoinedFactory({
+ let isDeleteCalled = false
+
+ await onWorkspaceRoleUpdatedFactory({
getDefaultWorkspaceProjectRoleMapping: async () => ({
[Roles.Workspace.Admin]: Roles.Stream.Owner,
[Roles.Workspace.Member]: Roles.Stream.Contributor,
[Roles.Workspace.Guest]: null
}),
async *queryAllWorkspaceProjects() {
- expect.fail()
+ yield [{ id: 'test' } as StreamRecord]
+ },
+ deleteProjectRole: async () => {
+ isDeleteCalled = true
+ return undefined
},
upsertProjectRole: async () => {
expect.fail()
@@ -80,6 +86,8 @@ describe('Event handlers', () => {
userId: cryptoRandomString({ length: 10 }),
workspaceId: cryptoRandomString({ length: 10 })
})
+
+ expect(isDeleteCalled).to.be.true
})
it('assigns the mapped projects roles to all queried project', async () => {
const projectIds = [
@@ -92,7 +100,7 @@ describe('Event handlers', () => {
const projectRole = Roles.Stream.Reviewer
const storedRoles: { userId: string; role: StreamRoles; projectId: string }[] = []
- await onWorkspaceJoinedFactory({
+ await onWorkspaceRoleUpdatedFactory({
getDefaultWorkspaceProjectRoleMapping: async () => ({
[Roles.Workspace.Admin]: Roles.Stream.Owner,
[Roles.Workspace.Member]: projectRole,
@@ -103,6 +111,9 @@ describe('Event handlers', () => {
yield projIds.map((projId) => ({ id: projId } as unknown as StreamRecord))
}
},
+ deleteProjectRole: async () => {
+ expect.fail()
+ },
upsertProjectRole: async (args) => {
storedRoles.push(args)
return {} as StreamRecord
diff --git a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts
index b81b9e61c..f3c103569 100644
--- a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts
+++ b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts
@@ -14,7 +14,10 @@ import {
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
-import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
+import {
+ WorkspaceEvents,
+ WorkspaceEventsPayloads
+} from '@/modules/workspacesCore/domain/events'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import { expectToThrow } from '@/test/assertionHelper'
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
@@ -386,17 +389,21 @@ const buildDeleteWorkspaceRoleAndTestContext = (
context.eventData.eventName = eventName
context.eventData.payload = payload
+ switch (eventName) {
+ case 'workspace.role-deleted': {
+ const { userId } =
+ payload as WorkspaceEventsPayloads['workspace.role-deleted']
+ for (const project of context.workspaceProjects) {
+ context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
+ (role) => role.resourceId !== project.id && role.userId !== userId
+ )
+ }
+ break
+ }
+ }
+
return []
},
- async *queryAllWorkspaceProjects() {
- yield context.workspaceProjects
- },
- deleteProjectRole: async ({ projectId, userId }) => {
- context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
- (role) => role.resourceId !== projectId && role.userId !== userId
- )
- return {} as StreamRecord
- },
...dependencyOverrides
}
@@ -430,32 +437,47 @@ const buildUpdateWorkspaceRoleAndTestContext = (
context.eventData.eventName = eventName
context.eventData.payload = payload
- return []
- },
- async *queryAllWorkspaceProjects() {
- yield context.workspaceProjects
- },
- getDefaultWorkspaceProjectRoleMapping: mapWorkspaceRoleToInitialProjectRole,
- upsertProjectRole: async (role) => {
- const streamAcl: StreamAclRecord = {
- userId: role.userId,
- role: role.role,
- resourceId: role.projectId
+ switch (eventName) {
+ case 'workspace.role-deleted': {
+ const { userId } =
+ payload as WorkspaceEventsPayloads['workspace.role-deleted']
+ for (const project of context.workspaceProjects) {
+ context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
+ (role) => role.resourceId !== project.id && role.userId !== userId
+ )
+ }
+ break
+ }
+ case 'workspace.role-updated': {
+ const workspaceRole =
+ payload as WorkspaceEventsPayloads['workspace.role-updated']
+ const mapping = await mapWorkspaceRoleToInitialProjectRole({
+ workspaceId: workspaceRole.workspaceId
+ })
+
+ for (const project of context.workspaceProjects) {
+ const projectRole = mapping[workspaceRole.role]
+
+ if (!projectRole) {
+ continue
+ }
+
+ const streamAcl: StreamAclRecord = {
+ userId: workspaceRole.userId,
+ role: projectRole,
+ resourceId: project.id
+ }
+
+ context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
+ (acl) => acl.userId !== workspaceRole.userId
+ )
+ context.workspaceProjectRoles.push(streamAcl)
+ }
+ break
+ }
}
- context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
- (acl) => acl.userId !== role.userId
- )
- context.workspaceProjectRoles.push(streamAcl)
-
- return {} as StreamRecord
- },
- deleteProjectRole: async ({ userId }) => {
- context.workspaceProjectRoles = context.workspaceProjectRoles.filter(
- (acl) => acl.userId !== userId
- )
-
- return {} as StreamRecord
+ return []
},
...dependencyOverrides
}
@@ -589,9 +611,15 @@ describe('Workspace role services', () => {
await updateWorkspaceRole(role)
+ const payload = {
+ ...(context.eventData
+ .payload as WorkspaceEventsPayloads['workspace.role-updated'])
+ }
+ delete payload.flags
+
expect(context.eventData.isCalled).to.be.true
expect(context.eventData.eventName).to.equal(WorkspaceEvents.RoleUpdated)
- expect(context.eventData.payload).to.deep.equal(role)
+ expect(payload).to.deep.equal(role)
})
it('throws if attempting to remove the last admin in a workspace', async () => {
const userId = cryptoRandomString({ length: 10 })
diff --git a/packages/server/modules/workspacesCore/domain/events.ts b/packages/server/modules/workspacesCore/domain/events.ts
index dd3147d10..03f01ad1a 100644
--- a/packages/server/modules/workspacesCore/domain/events.ts
+++ b/packages/server/modules/workspacesCore/domain/events.ts
@@ -20,7 +20,10 @@ type WorkspaceCreatedPayload = Workspace & {
}
type WorkspaceUpdatedPayload = Workspace
type WorkspaceRoleDeletedPayload = Pick
-type WorkspaceRoleUpdatedPayload = Pick
+type WorkspaceRoleUpdatedPayload = Pick<
+ WorkspaceAcl,
+ 'userId' | 'workspaceId' | 'role'
+> & { flags?: { skipProjectRoleUpdatesFor: string[] } }
type WorkspaceJoinedFromDiscoveryPayload = {
userId: string
workspaceId: string
diff --git a/packages/ui-components/src/components/common/ProgressBar.vue b/packages/ui-components/src/components/common/ProgressBar.vue
index 24f399b4a..b2a97427f 100644
--- a/packages/ui-components/src/components/common/ProgressBar.vue
+++ b/packages/ui-components/src/components/common/ProgressBar.vue
@@ -1,7 +1,8 @@
-
+
@@ -16,4 +17,14 @@ const props = defineProps<{
}>()
const percentage = computed(() => (props.currentValue / props.maxValue) * 100)
+const colorClass = computed(() => {
+ if (percentage.value >= 100) {
+ return 'bg-danger'
+ }
+ if (percentage.value >= 80) {
+ return 'bg-warning'
+ }
+
+ return 'bg-success'
+})