feat(fe): Show workspace invitations on onboarding join page
feat(fe): Show workspace invitations on onboarding join page
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
logo: string
|
||||
logo?: string
|
||||
name: string
|
||||
clickable?: boolean
|
||||
bannerText?: string | null
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
<p class="text-center text-body-sm text-foreground-2 mb-8">
|
||||
{{ description }}
|
||||
</p>
|
||||
<WorkspaceInviteCard
|
||||
v-for="invite in localInvites"
|
||||
:key="`invite-${invite.id}`"
|
||||
:invite="invite"
|
||||
:is-accepted="invite.isAccepted"
|
||||
@accepted="onInviteAccepted"
|
||||
/>
|
||||
<WorkspaceDiscoverableWorkspacesCard
|
||||
v-for="workspace in workspacesToShow"
|
||||
:key="`discoverable-${workspace.id}`"
|
||||
@@ -34,17 +41,20 @@
|
||||
@request="moveToTop(workspace.id, WorkspaceJoinRequestStatus.Pending)"
|
||||
/>
|
||||
<FormButton
|
||||
v-if="!showAllWorkspaces && discoverableWorkspacesAndJoinRequestsCount > 3"
|
||||
v-if="!showAllWorkspaces && totalWorkspaceItems > 3"
|
||||
color="subtle"
|
||||
size="lg"
|
||||
full-width
|
||||
@click="showAllWorkspaces = true"
|
||||
>
|
||||
Show all ({{ discoverableWorkspacesAndJoinRequestsCount }})
|
||||
Show all ({{ totalWorkspaceItems }})
|
||||
</FormButton>
|
||||
<div class="mt-2 w-full flex flex-col gap-2">
|
||||
<FormButton
|
||||
v-if="hasDiscoverableJoinRequests && !isWorkspaceNewPlansEnabled"
|
||||
v-if="
|
||||
(hasDiscoverableJoinRequests || hasWorkspaceInvites) &&
|
||||
!isWorkspaceNewPlansEnabled
|
||||
"
|
||||
size="lg"
|
||||
full-width
|
||||
color="primary"
|
||||
@@ -65,7 +75,11 @@
|
||||
Continue to workspace
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="!hasDiscoverableJoinRequests && !isWorkspaceNewPlansEnabled"
|
||||
v-if="
|
||||
!hasDiscoverableJoinRequests &&
|
||||
!hasWorkspaceInvites &&
|
||||
!isWorkspaceNewPlansEnabled
|
||||
"
|
||||
size="lg"
|
||||
full-width
|
||||
color="subtle"
|
||||
@@ -84,6 +98,8 @@ import { workspaceCreateRoute, homeRoute } from '~~/lib/common/helpers/route'
|
||||
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
|
||||
import type { DiscoverableWorkspace_LimitedWorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { WorkspaceJoinRequestStatus } from '~/lib/common/generated/gql/graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { navigationWorkspaceInvitesQuery } from '~~/lib/navigation/graphql/queries'
|
||||
|
||||
const { logout } = useAuthManager()
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
@@ -94,16 +110,47 @@ const {
|
||||
hasDiscoverableJoinRequests
|
||||
} = useDiscoverableWorkspaces()
|
||||
|
||||
const { result: workspaceInviteResult } = useQuery(navigationWorkspaceInvitesQuery)
|
||||
|
||||
const showAllWorkspaces = ref(false)
|
||||
|
||||
const actionedInvites = ref<
|
||||
{
|
||||
id: string
|
||||
invite: (typeof workspaceInvites.value)[0]
|
||||
status: 'accepted' | 'declined'
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const workspaceInvites = computed(() => {
|
||||
return workspaceInviteResult.value?.activeUser?.workspaceInvites || []
|
||||
})
|
||||
|
||||
const remainingInvites = computed(() => {
|
||||
const actionedIds = new Set(actionedInvites.value.map((a) => a.id))
|
||||
return workspaceInvites.value.filter((invite) => !actionedIds.has(invite.id))
|
||||
})
|
||||
|
||||
const localInvites = computed(() => [
|
||||
...actionedInvites.value
|
||||
.filter((a) => a.status === 'accepted')
|
||||
.map((a) => ({ ...a.invite, isAccepted: true })),
|
||||
...remainingInvites.value.map((invite) => ({ ...invite, isAccepted: false }))
|
||||
])
|
||||
|
||||
const actionedWorkspaces = ref<
|
||||
(DiscoverableWorkspace_LimitedWorkspaceFragment & { requestStatus: string | null })[]
|
||||
>([])
|
||||
|
||||
const remainingWorkspaces = computed(() => {
|
||||
const actionedIds = new Set(actionedWorkspaces.value.map((w) => w.id))
|
||||
const inviteWorkspaceIds = new Set(
|
||||
localInvites.value.map((invite) => invite.workspaceId)
|
||||
)
|
||||
|
||||
return (discoverableWorkspacesAndJoinRequests.value || []).filter(
|
||||
(workspace) => !actionedIds.has(workspace.id)
|
||||
(workspace) =>
|
||||
!actionedIds.has(workspace.id) && !inviteWorkspaceIds.has(workspace.id)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -112,20 +159,44 @@ const localWorkspaces = computed(() => [
|
||||
...remainingWorkspaces.value
|
||||
])
|
||||
|
||||
const hasApprovedWorkspace = computed(() =>
|
||||
localWorkspaces.value.some(
|
||||
const hasApprovedWorkspace = computed(() => {
|
||||
const hasApprovedDiscoverable = localWorkspaces.value.some(
|
||||
(workspace) => workspace.requestStatus === WorkspaceJoinRequestStatus.Approved
|
||||
)
|
||||
)
|
||||
|
||||
const hasAcceptedInvite = localInvites.value.some((invite) => invite.isAccepted)
|
||||
|
||||
return hasApprovedDiscoverable || hasAcceptedInvite
|
||||
})
|
||||
|
||||
const hasWorkspaceInvites = computed(() => localInvites.value.length > 0)
|
||||
|
||||
const workspacesToShow = computed(() => {
|
||||
return showAllWorkspaces.value
|
||||
? localWorkspaces.value
|
||||
: localWorkspaces.value.slice(0, 3)
|
||||
if (showAllWorkspaces.value) {
|
||||
return localWorkspaces.value
|
||||
}
|
||||
|
||||
// Show up to 3 total cards (invites + discoverable workspaces)
|
||||
const inviteCount = localInvites.value.length
|
||||
const remainingSlots = Math.max(0, 3 - inviteCount)
|
||||
return localWorkspaces.value.slice(0, remainingSlots)
|
||||
})
|
||||
|
||||
const totalWorkspaceItems = computed(() => {
|
||||
return localInvites.value.length + discoverableWorkspacesAndJoinRequestsCount.value
|
||||
})
|
||||
|
||||
const description = computed(() => {
|
||||
if (discoverableWorkspacesAndJoinRequestsCount.value === 1) {
|
||||
const inviteCount = localInvites.value.length
|
||||
const discoverableCount = discoverableWorkspacesAndJoinRequestsCount.value
|
||||
|
||||
if (inviteCount > 0 && discoverableCount > 0) {
|
||||
return 'You have workspace invitations and we found workspaces that match your email domain'
|
||||
} else if (inviteCount > 0) {
|
||||
return inviteCount === 1
|
||||
? 'You have a workspace invitation'
|
||||
: 'You have workspace invitations'
|
||||
} else if (discoverableCount === 1) {
|
||||
return 'We found a workspace that matches your email domain'
|
||||
}
|
||||
return 'We found workspaces that match your email domain'
|
||||
@@ -140,4 +211,16 @@ const moveToTop = (workspaceId: string, newStatus: WorkspaceJoinRequestStatus) =
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onInviteAccepted = (inviteId: string) => {
|
||||
const invite = localInvites.value.find((inv) => inv.id === inviteId)
|
||||
|
||||
if (invite) {
|
||||
actionedInvites.value.unshift({
|
||||
id: inviteId,
|
||||
invite,
|
||||
status: 'accepted'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<WorkspaceCard
|
||||
:name="invite.workspaceName"
|
||||
:class="isAccepted ? '' : 'bg-foundation'"
|
||||
:banner-text="`${invite.invitedBy.name} invited you to join this workspace`"
|
||||
>
|
||||
<template #actions>
|
||||
<FormButton
|
||||
v-if="isAccepted"
|
||||
color="outline"
|
||||
size="sm"
|
||||
:icon-left="CheckIcon"
|
||||
disabled
|
||||
>
|
||||
Workspace joined
|
||||
</FormButton>
|
||||
<div v-else class="flex flex-col gap-2 sm:items-end">
|
||||
<FormButton color="primary" size="sm" :disabled="loading" @click="onAccept">
|
||||
Accept invitation
|
||||
</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
</WorkspaceCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { WorkspaceInviteCard_PendingWorkspaceCollaboratorFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useWorkspaceInviteManager } from '~/lib/workspaces/composables/management'
|
||||
import { CheckIcon } from '@heroicons/vue/20/solid'
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceInviteCard_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {
|
||||
id
|
||||
workspaceId
|
||||
workspaceSlug
|
||||
workspaceName
|
||||
invitedBy {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
invite: WorkspaceInviteCard_PendingWorkspaceCollaboratorFragment
|
||||
isAccepted?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'accepted', inviteId: string): void
|
||||
}>()
|
||||
|
||||
const { loading, accept } = useWorkspaceInviteManager(
|
||||
{
|
||||
invite: computed(() => props.invite)
|
||||
},
|
||||
{
|
||||
preventRedirect: true
|
||||
}
|
||||
)
|
||||
|
||||
const onAccept = async () => {
|
||||
emit('accepted', props.invite.id)
|
||||
await accept()
|
||||
}
|
||||
</script>
|
||||
@@ -161,6 +161,7 @@ type Documents = {
|
||||
"\n fragment WorkspaceDashboardProjectList_Workspace on Workspace {\n ...WorkspaceAddProjectMenu_Workspace\n id\n }\n": typeof types.WorkspaceDashboardProjectList_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": typeof 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": typeof types.WorkspaceInviteBlock_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspaceInviteCard_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceSlug\n workspaceName\n invitedBy {\n id\n name\n }\n }\n": typeof types.WorkspaceInviteCard_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {\n id\n user {\n id\n name\n }\n workspace {\n id\n }\n }\n": typeof types.WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragmentDoc,
|
||||
"\n fragment WorkspaceMoveProjectManager_ProjectBase on Project {\n id\n name\n modelCount: models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n }\n": typeof types.WorkspaceMoveProjectManager_ProjectBaseFragmentDoc,
|
||||
"\n fragment WorkspaceMoveProjectManager_Project on Project {\n ...WorkspaceMoveProjectManager_ProjectBase\n permissions {\n canMoveToWorkspace(workspaceId: $workspaceId) {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n slug\n permissions {\n canMoveProjectToWorkspace(projectId: $projectId) {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.WorkspaceMoveProjectManager_ProjectFragmentDoc,
|
||||
@@ -611,6 +612,7 @@ const documents: Documents = {
|
||||
"\n fragment WorkspaceDashboardProjectList_Workspace on Workspace {\n ...WorkspaceAddProjectMenu_Workspace\n id\n }\n": types.WorkspaceDashboardProjectList_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 WorkspaceInviteCard_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceSlug\n workspaceName\n invitedBy {\n id\n name\n }\n }\n": types.WorkspaceInviteCard_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\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 WorkspaceMoveProjectManager_ProjectBase on Project {\n id\n name\n modelCount: models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n }\n": types.WorkspaceMoveProjectManager_ProjectBaseFragmentDoc,
|
||||
"\n fragment WorkspaceMoveProjectManager_Project on Project {\n ...WorkspaceMoveProjectManager_ProjectBase\n permissions {\n canMoveToWorkspace(workspaceId: $workspaceId) {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n slug\n permissions {\n canMoveProjectToWorkspace(projectId: $projectId) {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.WorkspaceMoveProjectManager_ProjectFragmentDoc,
|
||||
@@ -1516,6 +1518,10 @@ export function graphql(source: "\n fragment WorkspaceInviteBanner_PendingWorks
|
||||
* 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 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"): (typeof documents)["\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"];
|
||||
/**
|
||||
* 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 WorkspaceInviteCard_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceSlug\n workspaceName\n invitedBy {\n id\n name\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteCard_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceSlug\n workspaceName\n invitedBy {\n id\n name\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
Reference in New Issue
Block a user