Feat: Request to join workspace (#3871)

This commit is contained in:
Mike
2025-01-24 14:05:53 +01:00
committed by GitHub
parent 7343ed6ba1
commit e798fc4e8a
19 changed files with 521 additions and 159 deletions
@@ -28,7 +28,6 @@
:size="buttonSize"
color="outline"
class="px-4"
:icon-left="CheckIcon"
:disabled="loading"
@click="onAcceptClick(token)"
>
@@ -52,7 +51,6 @@
<script setup lang="ts">
import type { MaybeNullOrUndefined, Optional } from '@speckle/shared'
import type { AvatarUserType } from '~/lib/user/composables/avatar'
import { CheckIcon } from '@heroicons/vue/24/solid'
import { usePostAuthRedirect } from '~/lib/auth/composables/postAuthRedirect'
import {
useNavigateToLogin,
@@ -125,7 +123,9 @@ const mainInfoBlockClasses = computed(() => {
const avatarSize = computed(() => (props.block ? 'xxl' : 'base'))
const buttonSize = computed(() => (props.block ? 'lg' : 'sm'))
const isForRegisteredUser = computed(() => !!props.invite.user?.id)
const acceptMessage = computed(() => (props.invite.workspace ? 'Join' : 'Accept'))
const acceptMessage = computed(() =>
props.invite.workspace ? 'Request to join' : 'Accept'
)
const declineMessage = computed(() => (props.invite.workspace ? 'Dismiss' : 'Decline'))
const onLoginSignupClick = async () => {
@@ -4,6 +4,7 @@
v-model:search="search"
search-placeholder="Search guests..."
:workspace="workspace"
show-invite-button
/>
<LayoutTable
class="mt-6 md:mt-8"
@@ -4,6 +4,7 @@
v-model:search="search"
search-placeholder="Search pending invites..."
:workspace="workspace"
show-invite-button
/>
<LayoutTable
class="mt-6 md:mt-8 mb-12"
@@ -0,0 +1,110 @@
<template>
<div>
<LayoutTable
class="mt-2 mb-12"
:columns="[
{ id: 'name', header: 'Name', classes: 'col-span-3' },
{ id: 'createdAt', header: 'Requested at', classes: 'col-span-3' },
{
id: 'actions',
header: '',
classes: 'col-span-3 lg:col-span-6 flex items-center justify-end'
}
]"
:items="joinRequests"
empty-message="There are no pending join requests"
>
<template #name="{ item }">
<div class="flex items-center gap-2">
<UserAvatar hide-tooltip :user="item.user" />
<span class="truncate text-body-xs text-foreground">
{{ item.user.name }}
</span>
</div>
</template>
<template #createdAt="{ item }">
<p class="text-body-xs text-foreground">
{{ formattedFullDate(item.createdAt) }}
</p>
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<FormButton color="outline" size="sm" @click="onApprove(item)">
Approve
</FormButton>
<FormButton color="outline" size="sm" @click="onDeny(item)">Deny</FormButton>
</div>
</template>
</LayoutTable>
<WorkspaceJoinRequestApproveDialog
v-model:open="showApproveJoinRequestDialog"
:join-request="requestToApprove"
/>
</div>
</template>
<script setup lang="ts">
import { type MaybeNullOrUndefined } from '@speckle/shared'
import type {
SettingsWorkspacesMembersRequestsTable_WorkspaceFragment,
WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment
} from '~~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { useWorkspaceJoinRequest } from '~/lib/workspaces/composables/joinRequests'
graphql(`
fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {
...SettingsWorkspacesMembersTableHeader_Workspace
id
adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {
totalCount
items {
...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest
id
createdAt
status
user {
id
avatar
name
}
}
}
}
`)
const props = defineProps<{
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersRequestsTable_WorkspaceFragment>
}>()
const { deny } = useWorkspaceJoinRequest()
const showApproveJoinRequestDialog = ref(false)
const requestToApprove =
ref<WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment>()
const joinRequests = computed(
() => props.workspace?.adminWorkspacesJoinRequests?.items || []
)
const onApprove = (
request: WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment
) => {
requestToApprove.value = request
showApproveJoinRequestDialog.value = true
}
const onDeny = async (
request: WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment
) => {
if (!props.workspace?.id) return
await deny(
{
workspaceId: props.workspace?.id,
userId: request.user.id
},
request.id
)
}
</script>
@@ -6,6 +6,7 @@
search-placeholder="Search members..."
:workspace="workspace"
show-role-filter
show-invite-button
/>
<LayoutTable
class="mt-6 md:mt-8 mb-12"
@@ -24,12 +24,14 @@
:hide-items="[Roles.Workspace.Guest]"
/>
</div>
<div v-if="!isWorkspaceAdmin" v-tippy="'You must be a workspace admin'">
<FormButton :disabled="!isWorkspaceAdmin">Invite</FormButton>
</div>
<FormButton v-else @click="() => (isInviteDialogOpen = !isInviteDialogOpen)">
Invite
</FormButton>
<template v-if="showInviteButton">
<div v-if="!isWorkspaceAdmin" v-tippy="'You must be a workspace admin'">
<FormButton :disabled="!isWorkspaceAdmin">Invite</FormButton>
</div>
<FormButton v-else @click="() => (isInviteDialogOpen = !isInviteDialogOpen)">
Invite
</FormButton>
</template>
</div>
<InviteDialogWorkspace
v-if="workspace"
@@ -57,6 +59,7 @@ const props = defineProps<{
searchPlaceholder: string
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersTableHeader_WorkspaceFragment>
showRoleFilter?: boolean
showInviteButton?: boolean
}>()
const search = defineModel<string>('search')
@@ -1,7 +1,7 @@
<template>
<InviteBanner :invite="invite" @processed="processJoin">
<InviteBanner :invite="invite" @processed="processRequest">
<template #message>
Your team is already using Workspaces! Collaborate in the
Your team is already using Workspaces, request to join the
<span class="font-medium">{{ workspace.name }}</span>
space!
</template>
@@ -9,21 +9,17 @@
</template>
<script setup lang="ts">
import { useApolloClient } from '@vue/apollo-composable'
import { useMutation } from '@vue/apollo-composable'
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
import { graphql } from '~/lib/common/generated/gql'
import {
DashboardJoinWorkspaceDocument,
type WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { type WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment } from '~/lib/common/generated/gql/graphql'
import { CookieKeys } from '~/lib/common/helpers/constants'
import {
getCacheId,
getFirstErrorMessage,
modifyObjectField
} from '~/lib/common/helpers/graphql'
import { workspaceRoute } from '~/lib/common/helpers/route'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { dashboardRequestToJoinWorkspaceMutation } from '~~/lib/dashboard/graphql/mutations'
import {
convertThrowIntoFetchResult,
getFirstErrorMessage
} from '~~/lib/common/helpers/graphql'
graphql(`
fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {
@@ -33,27 +29,15 @@ graphql(`
description
logo
}
fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {
id
name
description
createdAt
updatedAt
logo
domainBasedMembershipProtectionEnabled
discoverabilityEnabled
}
`)
const props = defineProps<{
workspace: WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment
}>()
const { mutate: requestToJoin } = useMutation(dashboardRequestToJoinWorkspaceMutation)
const mixpanel = useMixpanel()
const { client: apollo } = useApolloClient()
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
const router = useRouter()
const dismissedDiscoverableWorkspaces = useSynchronizedCookie<string[]>(
CookieKeys.DismissedDiscoverableWorkspaces,
{
@@ -69,73 +53,55 @@ const invite = computed(() => ({
}
}))
const processJoin = async (accept: boolean) => {
if (!accept) {
const processRequest = async (accept: boolean) => {
if (accept) {
const result = await requestToJoin({
input: { workspaceId: props.workspace.id }
}).catch(convertThrowIntoFetchResult)
if (result?.data) {
// Dismiss it show it doesnt show again
dismissedDiscoverableWorkspaces.value = [
...dismissedDiscoverableWorkspaces.value,
props.workspace.id
]
mixpanel.track('Workspace Join Request Sent', {
workspaceId: props.workspace.id,
location: 'discovery banner',
// eslint-disable-next-line camelcase
workspace_id: props.workspace.id
})
triggerNotification({
title: 'Request sent',
description: 'Your request to join the workspace has been sent.',
type: ToastNotificationType.Success
})
} else {
const errorMessage = getFirstErrorMessage(result?.errors)
triggerNotification({
title: 'Failed to send request',
description: errorMessage,
type: ToastNotificationType.Danger
})
}
} else {
dismissedDiscoverableWorkspaces.value = [
...dismissedDiscoverableWorkspaces.value,
props.workspace.id
]
apollo.cache.evict({
id: getCacheId('LimitedWorkspace', props.workspace.id)
})
return
}
const userId = activeUser.value?.id
if (!userId) return
const result = await apollo
.mutate({
mutation: DashboardJoinWorkspaceDocument,
variables: {
input: {
workspaceId: props.workspace.id
}
},
update(cache, { data }) {
const workspaceId = data?.workspaceMutations.join.id
if (!workspaceId) return
modifyObjectField(
cache,
getCacheId('User', userId),
'workspaces',
({ variables, helpers: { evict, createUpdatedValue, ref } }) => {
if (variables.filter?.search?.length) return evict()
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount + 1)
update('items', (items) => [...items, ref('Workspace', workspaceId)])
})
}
)
}
})
.catch(convertThrowIntoFetchResult)
if (result?.data) {
apollo.cache.evict({
id: getCacheId('LimitedWorkspace', props.workspace.id)
})
triggerNotification({
type: ToastNotificationType.Success,
title: 'Joined workspace',
description: 'Successfully joined workspace'
})
mixpanel.track('Workspace Joined', {
mixpanel.track('Workspace Discovery Banner Dismissed', {
workspaceId: props.workspace.id,
location: 'discovery banner',
// eslint-disable-next-line camelcase
workspace_id: props.workspace.id
})
router.push(workspaceRoute(props.workspace.slug))
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to join workspace',
description: getFirstErrorMessage(result?.errors)
title: 'Discoverable workspace dismissed',
type: ToastNotificationType.Info
})
}
}
@@ -0,0 +1,72 @@
<template>
<LayoutDialog v-model:open="open" max-width="xs" :buttons="dialogButtons">
<template #header>Approve join request</template>
<p class="text-body-xs text-foreground">
Are you sure you want to approve
<span class="font-semibold">{{ joinRequest?.user.name }}'s</span>
join request?
</p>
<div class="text-body-2xs text-foreground-2 leading-5 mt-4 mb-1">
<p>
Adding users may add seats to your current billing cycle. If there are available
seats, they will be used first.
</p>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useWorkspaceJoinRequest } from '~/lib/workspaces/composables/joinRequests'
import { graphql } from '~/lib/common/generated/gql'
import type { WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment } from '~/lib/common/generated/gql/graphql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
graphql(`
fragment WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {
id
user {
id
name
}
workspace {
id
}
}
`)
const props = defineProps<{
joinRequest: MaybeNullOrUndefined<WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment>
}>()
const open = defineModel<boolean>('open', { required: true })
const { approve } = useWorkspaceJoinRequest()
const dialogButtons = computed((): LayoutDialogButton[] => {
return [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => {
open.value = false
}
},
{
text: 'Approve',
onClick: async () => {
if (props.joinRequest?.workspace.id && props.joinRequest?.user.id) {
await approve(
{
workspaceId: props.joinRequest.workspace.id,
userId: props.joinRequest.user.id
},
props.joinRequest.id
)
open.value = false
}
}
}
]
})
</script>
@@ -125,6 +125,7 @@ const documents = {
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n inviteId\n role\n title\n updatedAt\n user {\n id\n ...LimitedUserAvatar\n }\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n }\n": types.SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam(filter: $invitesFilter) {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n": types.SettingsWorkspacesMembersInvitesTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersRequestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n verified\n workspaceDomainPolicyCompliant\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersMembersTable_Workspace on Workspace {\n id\n name\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\n }\n": types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
@@ -143,7 +144,8 @@ const documents = {
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceBase_Workspace\n ...WorkspaceTeam_Workspace\n ...BillingAlert_Workspace\n slug\n readOnly\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
"\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBanner_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspaceInviteBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceName\n token\n user {\n id\n name\n ...LimitedUserAvatar\n }\n title\n email\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBlock_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragmentDoc,
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragmentDoc,
"\n fragment WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {\n id\n user {\n id\n name\n }\n workspace {\n id\n }\n }\n": types.WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragmentDoc,
"\n fragment WorkspaceSidebarAbout_Workspace on Workspace {\n ...WorkspaceDashboardAbout_Workspace\n }\n": types.WorkspaceSidebarAbout_WorkspaceFragmentDoc,
"\n fragment WorkspaceSidebarMembers_Workspace on Workspace {\n ...WorkspaceTeam_Workspace\n }\n": types.WorkspaceSidebarMembers_WorkspaceFragmentDoc,
"\n fragment WorkspaceSidebarSecurity_Workspace on Workspace {\n ...WorkspaceSecurity_Workspace\n }\n": types.WorkspaceSidebarSecurity_WorkspaceFragmentDoc,
@@ -179,7 +181,7 @@ const documents = {
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n": types.MainServerInfoDataDocument,
"\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n": types.DashboardJoinWorkspaceDocument,
"\n mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n": types.DashboardRequestToJoinWorkspaceDocument,
"\n query DashboardProjectsPageQuery {\n activeUser {\n id\n projects(limit: 3) {\n items {\n ...DashboardProjectCard_Project\n }\n }\n ...ProjectsDashboardHeaderProjects_User\n }\n }\n": types.DashboardProjectsPageQueryDocument,
"\n query DashboardProjectsPageWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.DashboardProjectsPageWorkspaceQueryDocument,
"\n mutation DeleteAccessToken($token: String!) {\n apiTokenRevoke(token: $token)\n }\n": types.DeleteAccessTokenDocument,
@@ -306,8 +308,9 @@ const documents = {
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": types.SettingsWorkspaceBillingCustomerPortalDocument,
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": types.SettingsWorkspaceRegionsDocument,
"\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
"\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
"\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter) {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersSearchDocument,
"\n query SettingsWorkspacesJoinRequestsSearch(\n $slug: String!\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n": types.SettingsWorkspacesJoinRequestsSearchDocument,
"\n query SettingsWorkspacesInvitesSearch(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesInvitesSearchDocument,
"\n query SettingsUserEmailsQuery {\n activeUser {\n ...SettingsUserEmails_User\n }\n }\n": types.SettingsUserEmailsQueryDocument,
"\n query SettingsWorkspacesProjects(\n $slug: String!\n $limit: Int!\n $cursor: String\n $filter: WorkspaceProjectsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n readOnly\n projects(limit: $limit, cursor: $cursor, filter: $filter) {\n cursor\n ...SettingsWorkspacesProjects_ProjectCollection\n }\n }\n }\n": types.SettingsWorkspacesProjectsDocument,
@@ -357,6 +360,8 @@ const documents = {
"\n mutation SetWorkspaceCreationState($input: WorkspaceCreationStateInput!) {\n workspaceMutations {\n updateCreationState(input: $input)\n }\n }\n": types.SetWorkspaceCreationStateDocument,
"\n mutation WorkspaceUpdateDomainProtectionMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n domainBasedMembershipProtectionEnabled\n }\n }\n }\n": types.WorkspaceUpdateDomainProtectionMutationDocument,
"\n mutation WorkspaceUpdateDiscoverabilityMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityEnabled\n }\n }\n }\n": types.WorkspaceUpdateDiscoverabilityMutationDocument,
"\n mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n approve(input: $input)\n }\n }\n": types.ApproveWorkspaceJoinRequestDocument,
"\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n": types.DenyWorkspaceJoinRequestDocument,
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": types.WorkspaceAccessCheckDocument,
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $token: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspaceProjectList_Workspace\n }\n workspaceInvite(\n workspaceId: $workspaceSlug\n token: $token\n options: { useSlug: true }\n ) {\n id\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n ...WorkspaceInviteBlock_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspacePageQueryDocument,
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspaceProjectsQueryDocument,
@@ -386,7 +391,7 @@ const documents = {
"\n fragment SettingsUserEmails_User on User {\n id\n emails {\n ...SettingsUserEmailCards_UserEmail\n }\n }\n": types.SettingsUserEmails_UserFragmentDoc,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n defaultProjectRole\n plan {\n status\n name\n }\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n }\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
@@ -851,6 +856,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembersInvitesTa
* 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 SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam(filter: $invitesFilter) {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam(filter: $invitesFilter) {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n"];
/**
* 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 SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -926,7 +935,11 @@ export function graphql(source: "\n fragment WorkspaceInviteBlock_PendingWorksp
/**
* 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 WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"];
export function graphql(source: "\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n"];
/**
* 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 WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {\n id\n user {\n id\n name\n }\n workspace {\n id\n }\n }\n"): (typeof documents)["\n fragment WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {\n id\n user {\n id\n name\n }\n workspace {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1070,7 +1083,7 @@ export function graphql(source: "\n query MainServerInfoData {\n serverInfo
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n"): (typeof documents)["\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n"];
export function graphql(source: "\n mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1578,11 +1591,15 @@ export function graphql(source: "\n query SettingsWorkspaceRegions($slug: Strin
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n"];
export function graphql(source: "\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter) {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter) {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspacesJoinRequestsSearch(\n $slug: String!\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesJoinRequestsSearch(\n $slug: String!\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1779,6 +1796,14 @@ export function graphql(source: "\n mutation WorkspaceUpdateDomainProtectionMut
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation WorkspaceUpdateDiscoverabilityMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityEnabled\n }\n }\n }\n"): (typeof documents)["\n mutation WorkspaceUpdateDiscoverabilityMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityEnabled\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n approve(input: $input)\n }\n }\n"): (typeof documents)["\n mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n approve(input: $input)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1898,7 +1923,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesGeneral_Workspac
/**
* 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 SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n }\n"];
export function graphql(source: "\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
import { graphql } from '~~/lib/common/generated/gql'
export const dashboardJoinWorkspaceMutation = graphql(`
mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {
export const dashboardRequestToJoinWorkspaceMutation = graphql(`
mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {
workspaceMutations {
join(input: $input) {
...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace
}
requestToJoin(input: $input)
}
}
`)
@@ -57,12 +57,14 @@ export const settingsWorkspacesMembersQuery = graphql(`
query SettingsWorkspacesMembers(
$slug: String!
$invitesFilter: PendingWorkspaceCollaboratorsFilter
$joinRequestsFilter: AdminWorkspaceJoinRequestFilter
) {
workspaceBySlug(slug: $slug) {
...SettingsWorkspacesMembers_Workspace
...SettingsWorkspacesMembersMembersTable_Workspace
...SettingsWorkspacesMembersGuestsTable_Workspace
...SettingsWorkspacesMembersInvitesTable_Workspace
...SettingsWorkspacesMembersRequestsTable_Workspace
}
}
`)
@@ -81,6 +83,18 @@ export const settingsWorkspacesMembersSearchQuery = graphql(`
}
`)
export const settingsWorkspacesJoinRequestsSearchQuery = graphql(`
query SettingsWorkspacesJoinRequestsSearch(
$slug: String!
$joinRequestsFilter: AdminWorkspaceJoinRequestFilter
) {
workspaceBySlug(slug: $slug) {
id
...SettingsWorkspacesMembersRequestsTable_Workspace
}
}
`)
export const settingsWorkspacesInvitesSearchQuery = graphql(`
query SettingsWorkspacesInvitesSearch(
$slug: String!
@@ -0,0 +1,109 @@
import { useMutation } from '@vue/apollo-composable'
import {
approveWorkspaceJoinRequestMutation,
denyWorkspaceJoinRequestMutation
} from '~/lib/workspaces/graphql/mutations'
import type {
ApproveWorkspaceJoinRequestInput,
DenyWorkspaceJoinRequestInput
} from '~~/lib/common/generated/gql/graphql'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
import {
convertThrowIntoFetchResult,
getFirstErrorMessage,
modifyObjectField,
getCacheId
} from '~~/lib/common/helpers/graphql'
export const useWorkspaceJoinRequest = () => {
const { mutate: approveMutation } = useMutation(approveWorkspaceJoinRequestMutation)
const { mutate: denyMutation } = useMutation(denyWorkspaceJoinRequestMutation)
const { triggerNotification } = useGlobalToast()
const approve = async (
input: ApproveWorkspaceJoinRequestInput,
requestId: string
) => {
const result = await approveMutation(
{ input },
{
update: (cache) => {
cache.evict({
id: getCacheId('WorkspaceJoinRequest', requestId)
})
modifyObjectField(
cache,
getCacheId('Workspace', input.workspaceId),
'adminWorkspacesJoinRequests',
({ helpers: { createUpdatedValue } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount - 1)
})
},
{
autoEvictFiltered: true
}
)
}
}
).catch(convertThrowIntoFetchResult)
if (result?.data) {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Workspace join request approved'
})
} else {
const errorMessage = getFirstErrorMessage(result?.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Workspace join request approval failed',
description: errorMessage
})
}
}
const deny = async (input: DenyWorkspaceJoinRequestInput, requestId: string) => {
const result = await denyMutation(
{ input },
{
update: (cache) => {
cache.evict({
id: getCacheId('WorkspaceJoinRequest', requestId)
})
modifyObjectField(
cache,
getCacheId('Workspace', input.workspaceId),
'adminWorkspacesJoinRequests',
({ helpers: { createUpdatedValue } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount - 1)
})
},
{
autoEvictFiltered: true
}
)
}
}
).catch(convertThrowIntoFetchResult)
if (result?.data) {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Workspace join request denied'
})
} else {
const errorMessage = getFirstErrorMessage(result?.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Workspace join request denial failed',
description: errorMessage
})
}
}
return { approve, deny }
}
@@ -105,3 +105,19 @@ export const workspaceUpdateDiscoverabilityMutation = graphql(`
}
}
`)
export const approveWorkspaceJoinRequestMutation = graphql(`
mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {
workspaceJoinRequestMutations {
approve(input: $input)
}
}
`)
export const denyWorkspaceJoinRequestMutation = graphql(`
mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {
workspaceJoinRequestMutations {
deny(input: $input)
}
}
`)
@@ -24,6 +24,11 @@
:workspace="workspace"
:workspace-slug="slug"
/>
<SettingsWorkspacesMembersJoinRequestsTable
v-if="activeItem.id === 'joinRequests'"
:workspace="workspace"
:workspace-slug="slug"
/>
</template>
</LayoutTabsHorizontal>
</div>
@@ -36,6 +41,8 @@ import { useQuery } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import { settingsWorkspacesMembersQuery } from '~/lib/settings/graphql/queries'
import type { LayoutPageTabItem } from '~~/lib/layout/helpers/components'
import { useOnWorkspaceUpdated } from '~/lib/workspaces/composables/management'
import { WorkspaceJoinRequestStatus } from '~/lib/common/generated/gql/graphql'
graphql(`
fragment SettingsWorkspacesMembers_Workspace on Workspace {
@@ -44,7 +51,6 @@ graphql(`
team {
items {
id
role
}
}
invitedTeam(filter: $invitesFilter) {
@@ -52,6 +58,9 @@ graphql(`
id
}
}
adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {
totalCount
}
}
`)
@@ -63,11 +72,14 @@ useHead({
title: 'Settings | Workspace - Members'
})
const route = useRoute()
const slug = computed(() => (route.params.slug as string) || '')
const route = useRoute()
const { result } = useQuery(settingsWorkspacesMembersQuery, () => ({
slug: slug.value
slug: slug.value,
joinRequestsFilter: {
status: WorkspaceJoinRequestStatus.Pending
}
}))
const workspace = computed(() => result.value?.workspaceBySlug)
@@ -83,6 +95,9 @@ const guestCount = computed(
.length
)
const invitedCount = computed(() => workspace.value?.invitedTeam?.length)
const joinRequestCount = computed(
() => workspace.value?.adminWorkspacesJoinRequests?.totalCount
)
const tabItems = computed<LayoutPageTabItem[]>(() => [
{ title: 'Members', id: 'members', count: memberCount.value },
{ title: 'Guests', id: 'guests', count: guestCount.value },
@@ -92,8 +107,17 @@ const tabItems = computed<LayoutPageTabItem[]>(() => [
disabled: !isAdmin.value,
disabledMessage: 'Only workspace admins can manage invites',
count: invitedCount.value
},
{
title: 'Join requests',
id: 'joinRequests',
disabled: !isAdmin.value,
disabledMessage: 'Only workspace admins can manage join requests',
count: joinRequestCount.value
}
])
const activeTab = ref(tabItems.value[0])
useOnWorkspaceUpdated({ workspaceSlug: slug })
</script>
@@ -123,7 +123,6 @@ import type { ShallowRef } from 'vue'
import { useQuery, useMutation } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import type { SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment } from '~/lib/common/generated/gql/graphql'
import { getFirstErrorMessage } from '~/lib/common/helpers/graphql'
import { settingsWorkspacesSecurityQuery } from '~/lib/settings/graphql/queries'
import { useAddWorkspaceDomain } from '~/lib/settings/composables/management'
import { useMixpanel } from '~/lib/core/composables/mp'
@@ -170,7 +169,6 @@ const slug = computed(() => (route.params.slug as string) || '')
const route = useRoute()
const addWorkspaceDomain = useAddWorkspaceDomain()
const { triggerNotification } = useGlobalToast()
const isSsoEnabled = useIsWorkspacesSsoEnabled()
const mixpanel = useMixpanel()
const { mutate: updateDomainProtection } = useMutation(
@@ -226,17 +224,6 @@ const isDomainProtectionEnabled = computed({
// eslint-disable-next-line camelcase
workspace_id: workspace.value?.id
})
triggerNotification({
type: ToastNotificationType.Success,
title: `Domain protection ${newVal ? 'enabled' : 'disabled'}`
})
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to update',
description: getFirstErrorMessage(result?.errors)
})
}
}
})
@@ -259,17 +246,6 @@ const isDomainDiscoverabilityEnabled = computed({
// eslint-disable-next-line camelcase
workspace_id: workspace.value?.id
})
triggerNotification({
type: ToastNotificationType.Success,
title: `Discoverability ${newVal ? 'enabled' : 'disabled'}`
})
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to update',
description: getFirstErrorMessage(result?.errors)
})
}
}
})
@@ -6,6 +6,7 @@ import { getUserFactory } from '@/modules/core/repositories/users'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { commandFactory } from '@/modules/shared/command'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems'
import {
ApproveWorkspaceJoinRequest,
@@ -30,6 +31,8 @@ import {
import { WorkspaceJoinRequestStatus } from '@/modules/workspacesCore/domain/types'
import { WorkspaceJoinRequestGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'
const eventBus = getEventBus()
export default {
Workspace: {
adminWorkspacesJoinRequests: async (parent, args, ctx) => {
@@ -78,7 +81,8 @@ export default {
approve: async (_parent, args) => {
const approveWorkspaceJoinRequest = commandFactory<ApproveWorkspaceJoinRequest>({
db,
operationFactory: ({ db }) => {
eventBus,
operationFactory: ({ db, emit }) => {
const updateWorkspaceJoinRequestStatus =
updateWorkspaceJoinRequestStatusFactory({
db
@@ -98,7 +102,8 @@ export default {
getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({
db
}),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db })
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
emit
})
}
})
@@ -21,6 +21,8 @@ import {
import { Roles } from '@speckle/shared'
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
import { EventBus } from '@/modules/shared/services/eventBus'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
export const dismissWorkspaceJoinRequestFactory =
({
@@ -104,7 +106,8 @@ export const approveWorkspaceJoinRequestFactory =
getUserById,
getWorkspace,
getWorkspaceJoinRequest,
upsertWorkspaceRole
upsertWorkspaceRole,
emit
}: {
updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus
sendWorkspaceJoinRequestApprovedEmail: SendWorkspaceJoinRequestApprovedEmail
@@ -112,6 +115,7 @@ export const approveWorkspaceJoinRequestFactory =
getWorkspace: GetWorkspace
getWorkspaceJoinRequest: GetWorkspaceJoinRequest
upsertWorkspaceRole: UpsertWorkspaceRole
emit: EventBus['emit']
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
const requester = await getUserById(userId)
@@ -142,6 +146,8 @@ export const approveWorkspaceJoinRequestFactory =
const role = Roles.Workspace.Member
await upsertWorkspaceRole({ userId, workspaceId, role, createdAt: new Date() })
await emit({ eventName: WorkspaceEvents.Updated, payload: { workspace } })
await sendWorkspaceJoinRequestApprovedEmail({
workspace,
requester
@@ -245,7 +245,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => null,
getWorkspace: async () => null,
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
upsertWorkspaceRole: async () => Promise.resolve(),
emit: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -261,7 +262,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => null,
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
upsertWorkspaceRole: async () => Promise.resolve(),
emit: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -285,7 +287,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => workspace as unknown as Workspace,
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
upsertWorkspaceRole: async () => Promise.resolve(),
emit: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -343,7 +346,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => workspace as unknown as Workspace,
getWorkspaceJoinRequest: async () => request,
upsertWorkspaceRole
upsertWorkspaceRole,
emit: async () => Promise.resolve()
})({ workspaceId: workspace.id, userId: user.id })
).to.equal(true)