Feat: New navigation (#4179)

This commit is contained in:
Mike
2025-03-13 15:23:41 +01:00
committed by GitHub
parent 629a7b10d8
commit ec435df79d
30 changed files with 1132 additions and 205 deletions
@@ -0,0 +1,188 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
<template>
<div class="group h-full">
<template v-if="isLoggedIn">
<Portal to="mobile-navigation">
<div class="lg:hidden">
<FormButton
:color="isOpenMobile ? 'outline' : 'subtle'"
size="sm"
class="mt-px"
@click="isOpenMobile = !isOpenMobile"
>
<IconSidebar v-if="!isOpenMobile" class="h-4 w-4 -ml-1 -mr-1" />
<IconSidebarClose v-else class="h-4 w-4 -ml-1 -mr-1" />
</FormButton>
</div>
</Portal>
<div
v-keyboard-clickable
class="lg:hidden absolute inset-0 backdrop-blur-sm z-40 transition-all"
:class="isOpenMobile ? 'opacity-100' : 'opacity-0 pointer-events-none'"
@click="isOpenMobile = false"
/>
<div
class="absolute z-40 lg:static h-full flex w-[17rem] shrink-0 transition-all"
:class="isOpenMobile ? '' : '-translate-x-[17rem] lg:translate-x-0'"
>
<LayoutSidebar
class="border-r border-outline-3 px-2 pt-3 pb-2 bg-foundation-page"
>
<LayoutSidebarMenu>
<LayoutSidebarMenuGroup>
<template v-if="!isWorkspacesEnabled">
<NuxtLink :to="projectsRoute" @click="isOpenMobile = false">
<LayoutSidebarMenuGroupItem
label="Projects"
:active="isActive(projectsRoute)"
>
<template #icon>
<IconProjects class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
</template>
<NuxtLink
v-if="activeWorkspaceSlug"
:to="workspaceRoute(activeWorkspaceSlug)"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem
label="Home"
:active="route.name === 'workspaces-slug'"
>
<template #icon>
<HomeIcon class="size-4 stroke-[1.5px]" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<NuxtLink
v-else-if="isProjectsActive"
:to="projectsRoute"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem
label="Projects"
:active="isActive(projectsRoute)"
>
<template #icon>
<IconProjects class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<NuxtLink :to="connectorsRoute" @click="isOpenMobile = false">
<LayoutSidebarMenuGroupItem
label="Connectors"
:active="isActive(connectorsRoute)"
>
<template #icon>
<IconConnectors class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<NuxtLink :to="tutorialsRoute" @click="isOpenMobile = false">
<LayoutSidebarMenuGroupItem
label="Tutorials"
:active="isActive(tutorialsRoute)"
>
<template #icon>
<IconTutorials class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
</LayoutSidebarMenuGroup>
<LayoutSidebarMenuGroup title="Resources" collapsible>
<NuxtLink
to="https://speckle.community/"
target="_blank"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem label="Community forum" external>
<template #icon>
<IconCommunity class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<div @click="openFeedbackDialog">
<LayoutSidebarMenuGroupItem label="Give us feedback">
<template #icon>
<IconFeedback class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</div>
<NuxtLink
to="https://speckle.guide/"
target="_blank"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem label="Documentation" external>
<template #icon>
<IconDocumentation class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<NuxtLink
to="https://speckle.community/c/making-speckle/changelog"
target="_blank"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem label="Changelog" external>
<template #icon>
<IconChangelog class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
</LayoutSidebarMenuGroup>
</LayoutSidebarMenu>
</LayoutSidebar>
</div>
</template>
<FeedbackDialog v-model:open="showFeedbackDialog" />
</div>
</template>
<script setup lang="ts">
import {
FormButton,
LayoutSidebar,
LayoutSidebarMenu,
LayoutSidebarMenuGroup,
LayoutSidebarMenuGroupItem
} from '@speckle/ui-components'
import {
projectsRoute,
connectorsRoute,
workspaceRoute,
tutorialsRoute
} from '~/lib/common/helpers/route'
import { useRoute } from 'vue-router'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { HomeIcon } from '@heroicons/vue/24/outline'
import { useNavigation } from '~~/lib/navigation/composables/navigation'
const { isLoggedIn } = useActiveUser()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const route = useRoute()
const { activeWorkspaceSlug, isProjectsActive } = useNavigation()
const isOpenMobile = ref(false)
const showFeedbackDialog = ref(false)
const isActive = (...routes: string[]): boolean => {
return routes.some((routeTo) => route.path === routeTo)
}
const openFeedbackDialog = () => {
showFeedbackDialog.value = true
isOpenMobile.value = false
}
</script>
@@ -0,0 +1,39 @@
<template>
<div class="flex items-center">
<div class="w-6 flex-shrink-0">
<IconCheck v-if="isActive" class="w-4 h-4 mx-1 text-foreground" />
</div>
<MenuItem class="min-w-0 w-full">
<NuxtLink class="flex-1 min-w-0" @click="$emit('onClick')">
<LayoutSidebarMenuGroupItem
:label="name"
:tag="tag"
color-classes="bg-foundation-2 text-foreground-2"
>
<template #icon>
<WorkspaceAvatar
:name="name"
:logo="logo"
size="sm"
class="flex-shrink-0"
/>
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
</MenuItem>
</div>
</template>
<script setup lang="ts">
import { MenuItem } from '@headlessui/vue'
import type { MaybeNullOrUndefined } from '@speckle/shared'
defineEmits(['onClick'])
defineProps<{
isActive: boolean
name: string
logo?: MaybeNullOrUndefined<string>
tag?: string
}>()
</script>
@@ -0,0 +1,11 @@
<template>
<div class="h-full">
<DashboardSidebarNew v-if="isWorkspaceNewPlansEnabled" />
<DashboardSidebar v-else />
</div>
</template>
<script setup lang="ts">
// Temporary wrapper to hold both the old and new sidebars
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
</script>
@@ -4,7 +4,15 @@
<div
class="flex gap-4 items-center justify-between h-full w-screen py-4 px-3 sm:px-4"
>
<HeaderLogoBlock :active="false" to="/" class="hidden lg:flex lg:min-w-40" />
<div class="hidden lg:block">
<HeaderWorkspaceSwitcher v-if="showWorkspaceSwitcher" />
<HeaderLogoBlock
v-else
:active="false"
to="/"
class="hidden lg:flex lg:min-w-40"
/>
</div>
<div class="flex items-center truncate">
<ClientOnly>
<PortalTarget name="mobile-navigation"></PortalTarget>
@@ -18,16 +26,18 @@
<PortalTarget name="secondary-actions"></PortalTarget>
<PortalTarget name="primary-actions"></PortalTarget>
</ClientOnly>
<FormButton
v-if="!activeUser"
:to="loginUrl.fullPath"
color="outline"
class="hidden md:flex"
>
Sign in
</FormButton>
<!-- Profile dropdown -->
<HeaderNavUserMenu :login-url="loginUrl" />
<div class="flex justify-end">
<FormButton
v-if="!activeUser"
:to="loginUrl.fullPath"
color="outline"
class="hidden md:flex"
>
Sign in
</FormButton>
<!-- Profile dropdown -->
<HeaderNavUserMenu :login-url="loginUrl" />
</div>
</div>
</div>
</nav>
@@ -39,6 +49,8 @@ import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { loginRoute } from '~~/lib/common/helpers/route'
import type { Optional } from '@speckle/shared'
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const { activeUser } = useActiveUser()
const route = useRoute()
const router = useRouter()
@@ -53,4 +65,9 @@ const loginUrl = computed(() =>
}
})
)
const showWorkspaceSwitcher = computed(
() =>
isWorkspacesEnabled.value && isWorkspaceNewPlansEnabled.value && activeUser.value
)
</script>
@@ -19,77 +19,64 @@
<MenuItems
class="absolute right-4 top-14 w-56 origin-top-right bg-foundation outline outline-1 outline-primary-muted rounded-md shadow-lg overflow-hidden"
>
<div class="border-b border-outline-3 py-1 mb-1">
<div class="pt-1">
<MenuItem v-if="activeUser" v-slot="{ active }">
<NuxtLink
:to="settingsUserRoutes.profile"
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
>
Settings
</NuxtLink>
</MenuItem>
<MenuItem v-if="isAdmin" v-slot="{ active }">
<NuxtLink
:to="settingsServerRoutes.general"
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
>
Server settings
</NuxtLink>
</MenuItem>
<MenuItem v-slot="{ active }">
<NuxtLink
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-primary cursor-pointer transition mx-1 rounded'
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
target="_blank"
external
:href="downloadManagerUrl"
@click="toggleTheme"
>
Connector downloads
{{ isDarkTheme ? 'Light mode' : 'Dark mode' }}
</NuxtLink>
</MenuItem>
<MenuItem v-if="activeUser && !isGuest" v-slot="{ active }">
<NuxtLink
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="toggleInviteDialog"
>
Invite to Speckle
</NuxtLink>
</MenuItem>
<MenuItem v-slot="{ active }">
<NuxtLink
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
class="text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded"
@click="openFeedbackDialog"
>
Feedback
</NuxtLink>
</MenuItem>
</div>
<MenuItem v-if="activeUser" v-slot="{ active }">
<NuxtLink
:to="settingsUserRoutes.profile"
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
>
Settings
</NuxtLink>
</MenuItem>
<MenuItem v-if="isAdmin" v-slot="{ active }">
<NuxtLink
:to="settingsServerRoutes.general"
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
>
Server settings
</NuxtLink>
</MenuItem>
<MenuItem v-slot="{ active }">
<NuxtLink
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="toggleTheme"
>
{{ isDarkTheme ? 'Light mode' : 'Dark mode' }}
</NuxtLink>
</MenuItem>
<MenuItem v-if="activeUser && !isGuest" v-slot="{ active }">
<NuxtLink
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="toggleInviteDialog"
>
Invite to Speckle
</NuxtLink>
</MenuItem>
<MenuItem v-slot="{ active }">
<NuxtLink
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
class="text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded"
@click="openFeedbackDialog"
>
Feedback
</NuxtLink>
</MenuItem>
<div class="border-t border-outline-3 py-1 mt-1">
<MenuItem v-if="activeUser" v-slot="{ active }">
<NuxtLink
@@ -135,11 +122,7 @@ import { Roles } from '@speckle/shared'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { useAuthManager } from '~~/lib/auth/composables/auth'
import { useTheme } from '~~/lib/core/composables/theme'
import {
downloadManagerUrl,
settingsUserRoutes,
settingsServerRoutes
} from '~/lib/common/helpers/route'
import { settingsUserRoutes, settingsServerRoutes } from '~/lib/common/helpers/route'
import type { RouteLocationRaw } from 'vue-router'
import { useServerInfo } from '~/lib/core/composables/server'
@@ -0,0 +1,238 @@
<template>
<div>
<Menu as="div" class="flex items-center">
<MenuButton :id="menuButtonId" v-slot="{ open: userOpen }">
<span class="sr-only">Open workspace menu</span>
<div class="flex items-center gap-2 p-0.5 pr-1.5 hover:bg-highlight-2 rounded">
<template v-if="activeWorkspaceSlug || isProjectsActive">
<div class="relative">
<WorkspaceAvatar :name="displayName" :logo="displayLogo" />
<div
v-if="hasDiscoverableWorkspaces"
class="absolute -top-[4px] -right-[4px] size-3 border-[2px] border-foundation-page bg-primary rounded-full"
/>
</div>
<p class="text-body-xs text-foreground truncate max-w-40">
{{ displayName }}
</p>
</template>
<HeaderLogoBlock v-else no-link />
<ChevronDownIcon
:class="userOpen ? 'rotate-180' : ''"
class="h-3 w-3 flex-shrink-0"
/>
</div>
</MenuButton>
<Transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute left-4 top-14 w-64 origin-top-right bg-foundation outline outline-1 outline-primary-muted rounded-md shadow-lg overflow-hidden divide-y divide-outline-2"
>
<div
v-if="activeWorkspaceSlug || isProjectsActive"
class="p-2 pb-3 flex flex-col gap-y-4"
>
<div class="flex gap-x-2 items-center">
<MenuItem>
<NuxtLink
:to="
activeWorkspaceSlug
? workspaceRoute(activeWorkspaceSlug)
: projectsRoute
"
>
<WorkspaceAvatar
:name="displayName"
:logo="displayLogo"
size="lg"
class="flex-shrink-0"
/>
</NuxtLink>
</MenuItem>
<div class="flex flex-col space-between min-w-0">
<p class="text-body-xs text-foreground truncate">
{{ displayName }}
</p>
<p
v-if="activeWorkspace"
class="text-body-2xs text-foreground-2 capitalize truncate"
>
{{ activeWorkspace?.plan?.name }} ·
{{ activeWorkspace?.team?.totalCount }} member{{
activeWorkspace?.team?.totalCount > 1 ? 's' : ''
}}
</p>
<p v-else class="text-body-2xs text-foreground-2 truncate">
2 projects to move
</p>
</div>
</div>
<div v-if="activeWorkspaceSlug" class="flex gap-x-2">
<MenuItem>
<FormButton
color="outline"
full-width
size="sm"
@click="goToSettingsRoute"
>
Settings
</FormButton>
</MenuItem>
<MenuItem>
<FormButton
full-width
color="outline"
size="sm"
:disabled="activeWorkspace?.role !== Roles.Workspace.Admin"
@click="showInviteDialog = true"
>
Invite members
</FormButton>
</MenuItem>
</div>
</div>
<div class="p-2 pt-1">
<LayoutSidebarMenuGroup
title="Workspaces"
:icon-click="isGuest ? undefined : handlePlusClick"
icon-text="Create workspace"
>
<div v-if="hasWorkspaces" class="w-full">
<template v-for="item in workspaces" :key="`menu-item-${item.id}`">
<DashboardSidebarWorkspaceItem
:is-active="item.slug === activeWorkspaceSlug"
:name="item.name"
:logo="item.logo"
@on-click="onWorkspaceSelect(item.slug)"
/>
</template>
<DashboardSidebarWorkspaceItem
:is-active="route.path === projectsRoute"
name="Personal projects"
tag="LEGACY"
@on-click="onProjectsSelect"
/>
</div>
</LayoutSidebarMenuGroup>
</div>
<MenuItem v-if="hasDiscoverableWorkspacesOrJoinRequests">
<div class="p-2">
<NuxtLink
class="flex justify-between items-center cursor-pointer hover:bg-highlight-1 py-1 px-2 rounded"
@click="showDiscoverableWorkspacesModal = true"
>
<p class="text-body-xs text-foreground">Join existing workspaces</p>
<CommonBadge v-if="hasDiscoverableWorkspaces" rounded>
{{ discoverableWorkspacesCount }}
</CommonBadge>
</NuxtLink>
</div>
</MenuItem>
</MenuItems>
</Transition>
</Menu>
<InviteDialogWorkspace
v-model:open="showInviteDialog"
:workspace="activeWorkspace"
/>
<WorkspaceDiscoverableWorkspacesModal
v-model:open="showDiscoverableWorkspacesModal"
/>
</div>
</template>
<script setup lang="ts">
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import {
workspaceCreateRoute,
workspaceRoute,
settingsWorkspaceRoutes,
projectsRoute
} from '~/lib/common/helpers/route'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useUserWorkspaces } from '~/lib/user/composables/workspaces'
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
import { graphql } from '~/lib/common/generated/gql'
import { useNavigation } from '~~/lib/navigation/composables/navigation'
import { Roles } from '@speckle/shared'
graphql(`
fragment HeaderWorkspaceSwitcher_Workspace on Workspace {
...InviteDialogWorkspace_Workspace
id
name
logo
role
plan {
name
}
team {
totalCount
}
}
`)
const { isGuest } = useActiveUser()
const menuButtonId = useId()
const mixpanel = useMixpanel()
const {
activeWorkspaceSlug,
isProjectsActive,
mutateActiveWorkspaceSlug,
mutateIsProjectsActive,
workspaceData
} = useNavigation()
const showInviteDialog = ref(false)
const showDiscoverableWorkspacesModal = ref(false)
const activeWorkspace = computed(() => {
return workspaceData.value
})
const displayName = computed(() => activeWorkspace.value?.name || 'Personal projects')
const displayLogo = computed(() => {
if (isProjectsActive.value) return null
return activeWorkspace.value?.logo
})
const route = useRoute()
const { workspaces, hasWorkspaces } = useUserWorkspaces()
const {
hasDiscoverableWorkspaces,
discoverableWorkspacesCount,
hasDiscoverableWorkspacesOrJoinRequests
} = useDiscoverableWorkspaces()
const onWorkspaceSelect = (slug: string) => {
navigateTo(workspaceRoute(slug))
mutateActiveWorkspaceSlug(slug)
}
const onProjectsSelect = () => {
mutateIsProjectsActive(true)
navigateTo(projectsRoute)
}
const goToSettingsRoute = () => {
if (!activeWorkspaceSlug.value) return
navigateTo(settingsWorkspaceRoutes.general.route(activeWorkspaceSlug.value))
}
const handlePlusClick = () => {
navigateTo(workspaceCreateRoute())
mixpanel.track('Create Workspace Button Clicked', {
source: 'navigation'
})
}
</script>
@@ -29,7 +29,7 @@ import {
} from '~/lib/common/generated/gql/graphql'
import type { InviteGenericItem } from '~~/lib/invites/helpers/types'
import { emptyInviteGenericItem } from '~~/lib/invites/helpers/constants'
import { Roles } from '@speckle/shared'
import { Roles, type MaybeNullOrUndefined } from '@speckle/shared'
import { useMixpanel } from '~/lib/core/composables/mp'
import { mapMainRoleToGqlWorkspaceRole } from '~/lib/workspaces/helpers/roles'
import { mapServerRoleToGqlServerRole } from '~/lib/common/helpers/roles'
@@ -58,7 +58,7 @@ graphql(`
`)
const props = defineProps<{
workspace: InviteDialogWorkspace_WorkspaceFragment
workspace?: MaybeNullOrUndefined<InviteDialogWorkspace_WorkspaceFragment>
}>()
const isOpen = defineModel<boolean>('open', { required: true })
@@ -75,7 +75,7 @@ const invites = ref<InviteGenericItem[]>([
])
const allowedDomains = computed(() =>
props.workspace.domainBasedMembershipProtectionEnabled
props.workspace?.domainBasedMembershipProtectionEnabled
? props.workspace.domains?.map((d) => d.domain)
: null
)
@@ -117,7 +117,7 @@ const onSelectUsersSubmit = async (updatedInvites: InviteGenericItem[]) => {
: undefined
}))
if (!inputs.length) return
if (!inputs.length || !props.workspace?.id) return
await inviteToWorkspace({ workspaceId: props.workspace.id, inputs })
isOpen.value = false
@@ -37,12 +37,7 @@
</div>
<span v-else class="text-body-xs text-foreground-2 text-center select-none">
Use our
<NuxtLink
target="_blank"
:to="downloadManagerUrl"
class="font-medium"
@click.stop
>
<NuxtLink target="_blank" :to="connectorsRoute" class="font-medium" @click.stop>
<span class="underline">connectors</span>
</NuxtLink>
to publish a {{ modelName ? '' : 'new model' }} version to
@@ -56,7 +51,7 @@
import { useFileImport } from '~~/lib/core/composables/fileImport'
import { useFileUploadProgressCore } from '~~/lib/form/composables/fileUpload'
import { ExclamationTriangleIcon } from '@heroicons/vue/24/solid'
import { downloadManagerUrl } from '~/lib/common/helpers/route'
import { connectorsRoute } from '~/lib/common/helpers/route'
import type { Nullable } from '@speckle/shared'
const props = defineProps<{
@@ -2,6 +2,7 @@
<div>
<Portal to="primary-actions"></Portal>
<ProjectsDashboardHeader
v-if="!isWorkspaceNewPlansEnabled"
:projects-invites="projectsPanelResult?.activeUser"
:workspaces-invites="workspacesResult?.activeUser"
/>
@@ -92,6 +93,7 @@ const showLoadingBar = ref(false)
const areQueriesLoading = useQueryLoading()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const { isGuest } = useActiveUser()
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
useUserProjectsUpdatedTracking()
const {
@@ -110,7 +112,8 @@ const {
} = useQuery(projectsDashboardQuery, () => ({
filter: {
search: (search.value || '').trim() || null,
onlyWithRoles: selectedRoles.value?.length ? selectedRoles.value : null
onlyWithRoles: selectedRoles.value?.length ? selectedRoles.value : null,
workspaceId: isWorkspaceNewPlansEnabled ? (null as Nullable<string>) : undefined
},
cursor: null as Nullable<string>
}))
@@ -68,61 +68,103 @@
/>
</NuxtLink>
</LayoutSidebarMenuGroup>
<LayoutSidebarMenuGroup v-if="isWorkspacesEnabled" title="Workspace settings">
<LayoutSidebarMenuGroup
v-for="workspaceItem in workspaceItems"
:key="`workspace-item-${workspaceItem.slug}`"
:title="workspaceItem.name"
collapsible
:collapsed="slug !== workspaceItem.slug"
:tag="
workspaceItem.plan?.status === WorkspacePlanStatuses.Trial ||
!workspaceItem.plan?.status
? 'TRIAL'
: undefined
"
nested
>
<template #title-icon>
<WorkspaceAvatar
:logo="workspaceItem.logo"
:name="workspaceItem.name"
size="sm"
/>
</template>
<LayoutSidebarMenuGroup
v-if="showWorkspaceSettings"
title="Workspace settings"
>
<template v-if="isWorkspaceNewPlansEnabled" #title-icon>
<IconWorkspaces class="size-4" />
</template>
<template v-if="!isWorkspaceNewPlansEnabled">
<LayoutSidebarMenuGroup
v-for="workspaceItem in workspaceItems"
:key="`workspace-item-${workspaceItem.slug}`"
:title="workspaceItem.name"
collapsible
:collapsed="slug !== workspaceItem.slug"
:tag="
workspaceItem.plan?.status === WorkspacePlanStatuses.Trial ||
!workspaceItem.plan?.status
? 'TRIAL'
: undefined
"
nested
>
<template #title-icon>
<WorkspaceAvatar
:logo="workspaceItem.logo"
:name="workspaceItem.name"
size="sm"
/>
</template>
<NuxtLink
v-for="workspaceMenuItem in workspaceMenuItems"
:key="`workspace-menu-item-${workspaceMenuItem.name}-${workspaceItem.slug}`"
:to="
!isAdmin &&
(workspaceMenuItem.disabled ||
needsSsoSession(workspaceItem, workspaceMenuItem.name))
? undefined
: workspaceMenuItem.route(workspaceItem.slug)
"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem
v-if="workspaceMenuItem.permission?.includes(workspaceItem.role as WorkspaceRoles)"
:label="workspaceMenuItem.title"
:active="
route.name?.toString().startsWith(workspaceMenuItem.name) &&
route.params.slug === workspaceItem.slug
"
:tooltip-text="
needsSsoSession(workspaceItem, workspaceMenuItem.name)
? 'Log in with your SSO provider to access this page'
: workspaceMenuItem.tooltipText
"
:disabled="
!isAdmin &&
(workspaceMenuItem.disabled ||
needsSsoSession(workspaceItem, workspaceMenuItem.name))
"
class="!pl-8"
/>
</NuxtLink>
</LayoutSidebarMenuGroup>
</template>
<template v-else-if="activeWorkspaceItem">
<NuxtLink
v-for="workspaceMenuItem in workspaceMenuItems"
:key="`workspace-menu-item-${workspaceMenuItem.name}-${workspaceItem.slug}`"
:key="`workspace-menu-item-${workspaceMenuItem.name}-${activeWorkspaceItem}`"
:to="
!isAdmin &&
(workspaceMenuItem.disabled ||
needsSsoSession(workspaceItem, workspaceMenuItem.name))
needsSsoSession(activeWorkspaceItem, workspaceMenuItem.name))
? undefined
: workspaceMenuItem.route(workspaceItem.slug)
: workspaceMenuItem.route(activeWorkspaceItem.slug)
"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem
v-if="workspaceMenuItem.permission?.includes(workspaceItem.role as WorkspaceRoles)"
v-if="workspaceMenuItem.permission?.includes(activeWorkspaceItem.role as WorkspaceRoles)"
:label="workspaceMenuItem.title"
:active="
route.name?.toString().startsWith(workspaceMenuItem.name) &&
route.params.slug === workspaceItem.slug
route.params.slug === activeWorkspaceItem.slug
"
:tooltip-text="
needsSsoSession(workspaceItem, workspaceMenuItem.name)
needsSsoSession(activeWorkspaceItem, workspaceMenuItem.name)
? 'Log in with your SSO provider to access this page'
: workspaceMenuItem.tooltipText
"
:disabled="
!isAdmin &&
(workspaceMenuItem.disabled ||
needsSsoSession(workspaceItem, workspaceMenuItem.name))
needsSsoSession(activeWorkspaceItem, workspaceMenuItem.name))
"
class="!pl-8"
/>
</NuxtLink>
</LayoutSidebarMenuGroup>
</template>
</LayoutSidebarMenuGroup>
</LayoutSidebarMenu>
</LayoutSidebar>
@@ -144,16 +186,22 @@ import {
} from '@speckle/ui-components'
import { graphql } from '~~/lib/common/generated/gql'
import type { WorkspaceRoles } from '@speckle/shared'
import { homeRoute, settingsWorkspaceRoutes } from '~/lib/common/helpers/route'
import {
homeRoute,
projectsRoute,
settingsWorkspaceRoutes,
workspaceRoute
} from '~/lib/common/helpers/route'
import {
WorkspacePlanStatuses,
type SettingsMenu_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import { useBreakpoints } from '@vueuse/core'
import { useNavigation } from '~~/lib/navigation/composables/navigation'
graphql(`
fragment SettingsDialog_Workspace on Workspace {
fragment SettingsSidebar_Workspace on Workspace {
...SettingsMenu_Workspace
id
slug
@@ -162,6 +210,7 @@ graphql(`
logo
plan {
status
name
}
creationState {
completed
@@ -170,20 +219,22 @@ graphql(`
`)
graphql(`
fragment SettingsDialog_User on User {
fragment SettingsSidebar_User on User {
id
workspaces {
items {
...SettingsDialog_Workspace
...SettingsSidebar_Workspace
}
}
}
`)
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const { activeWorkspaceSlug } = useNavigation()
const settingsMenuState = useSettingsMenuState()
const { isAdmin } = useActiveUser()
const route = useRoute()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const { result: workspaceResult } = useQuery(settingsSidebarQuery, null, {
enabled: computed(() => isWorkspacesEnabled.value)
})
@@ -200,6 +251,9 @@ const workspaceItems = computed(
(item) => item.creationState?.completed !== false // Removed workspaces that are not completely created
) || []
)
const activeWorkspaceItem = computed(() =>
workspaceItems.value.find((item) => item.slug === activeWorkspaceSlug.value)
)
const needsSsoSession = (
workspace: SettingsMenu_WorkspaceFragment,
@@ -212,9 +266,19 @@ const needsSsoSession = (
}
const exitSettingsRoute = computed(() => {
if (import.meta.server || !settingsMenuState.value.previousRoute) {
return homeRoute
if (import.meta.server) return homeRoute
if (!settingsMenuState.value.previousRoute) {
return activeWorkspaceSlug.value
? workspaceRoute(activeWorkspaceSlug.value)
: projectsRoute
}
return settingsMenuState.value.previousRoute
})
const showWorkspaceSettings = computed(() => {
if (!isWorkspacesEnabled.value) return false
if (isWorkspaceNewPlansEnabled.value) return !!activeWorkspaceSlug.value
return true
})
</script>
@@ -7,7 +7,7 @@
>
<div
class="h-full w-full bg-cover bg-center bg-no-repeat flex items-center justify-center"
:style="logo ? { backgroundImage: `url('${logo}')` } : undefined"
:style="logo ? { backgroundImage: `url('${logo}')` } : {}"
>
<span v-if="!logo" class="text-foreground-3 uppercase leading-none">
{{ name[0] }}
@@ -14,7 +14,7 @@
<Portal v-if="workspace?.name" to="navigation">
<HeaderNavLink
:to="workspaceRoute(workspaceSlug)"
:name="workspace?.name"
:name="isWorkspaceNewPlansEnabled ? 'Home' : workspace?.name"
:separator="false"
/>
</Portal>
@@ -128,6 +128,10 @@ graphql(`
}
`)
const props = defineProps<{
workspaceSlug: string
}>()
const { validateCheckoutSession } = useBillingActions()
const areQueriesLoading = useQueryLoading()
const route = useRoute()
@@ -138,10 +142,7 @@ const {
} = useDebouncedTextInput({
debouncedBy: 800
})
const props = defineProps<{
workspaceSlug: string
}>()
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const showMoveProjectsDialog = ref(false)
const selectedRoles = ref(undefined as Optional<StreamRoles[]>)
@@ -0,0 +1,34 @@
<template>
<LayoutDialog v-model:open="open" max-width="md" :buttons="dialogButtons">
<template #header>Join existing workspaces</template>
<p class="text-body-xs text-foreground-2 pb-3">
Workspaces that match your email domain
</p>
<div class="flex flex-col gap-y-3">
<WorkspaceDiscoverableWorkspacesCard
v-for="workspace in discoverableWorkspacesAndJoinRequests"
:key="workspace.id"
:workspace="workspace"
/>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
const { discoverableWorkspacesAndJoinRequests } = useDiscoverableWorkspaces()
const open = defineModel<boolean>('open', { required: true })
const dialogButtons = computed((): LayoutDialogButton[] => {
return [
{
text: 'Close',
onClick: () => {
open.value = false
}
}
]
})
</script>
@@ -12,33 +12,40 @@
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3 lg:gap-4">
<WorkspaceAvatar
v-tippy="workspaceInfo.logo ? undefined : 'Add a workspace icon'"
:name="workspaceInfo.name"
:logo="workspaceInfo.logo"
size="lg"
class="hidden md:block"
:class="{ 'cursor-pointer': !workspaceInfo.logo }"
is-button
@click="
workspaceInfo.logo
? undefined
: navigateTo(settingsWorkspaceRoutes.general.route(workspaceInfo.slug))
"
/>
<WorkspaceAvatar
class="md:hidden"
:name="workspaceInfo.name"
:logo="workspaceInfo.logo"
/>
<h1 class="text-heading-sm md:text-heading line-clamp-2">
{{ workspaceInfo.name }}
</h1>
<CommonBadge rounded color-classes="bg-highlight-3 text-foreground-2">
<span class="capitalize">
{{ workspaceInfo.role?.split(':').reverse()[0] }}
</span>
</CommonBadge>
<template v-if="isWorkspaceNewPlansEnabled">
<h1 class="text-heading-sm md:text-heading line-clamp-2">
Hello, {{ activeUser?.name }}
</h1>
</template>
<template v-else>
<WorkspaceAvatar
v-tippy="workspaceInfo.logo ? undefined : 'Add a workspace icon'"
:name="workspaceInfo.name"
:logo="workspaceInfo.logo"
size="lg"
class="hidden md:block"
:class="{ 'cursor-pointer': !workspaceInfo.logo }"
is-button
@click="
workspaceInfo.logo
? undefined
: navigateTo(settingsWorkspaceRoutes.general.route(workspaceInfo.slug))
"
/>
<WorkspaceAvatar
class="md:hidden"
:name="workspaceInfo.name"
:logo="workspaceInfo.logo"
/>
<h1 class="text-heading-sm md:text-heading line-clamp-2">
{{ workspaceInfo.name }}
</h1>
<CommonBadge rounded color-classes="bg-highlight-3 text-foreground-2">
<span class="capitalize">
{{ workspaceInfo.role?.split(':').reverse()[0] }}
</span>
</CommonBadge>
</template>
</div>
<div class="flex gap-1.5 md:gap-2">
@@ -107,6 +114,9 @@ const props = defineProps<{
workspaceInfo: WorkspaceHeader_WorkspaceFragment
}>()
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const { activeUser } = useActiveUser()
const isWorkspaceAdmin = computed(
() => props.workspaceInfo.role === Roles.Workspace.Admin
)
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="h-12 w-full shrink-0"></div>
<div class="relative flex h-[calc(100dvh-3rem)]">
<DashboardSidebar />
<DashboardSidebarWrapper />
<main class="w-full h-full overflow-y-auto simple-scrollbar pt-4 lg:pt-6 pb-16">
<div class="container mx-auto px-6 md:px-8">
@@ -6,7 +6,7 @@
<div class="h-12 w-full shrink-0"></div>
<div class="relative flex h-[calc(100dvh-3rem)]">
<DashboardSidebar />
<DashboardSidebarWrapper />
<main class="w-full h-full overflow-y-auto simple-scrollbar pt-4 lg:pt-6 pb-16">
<div class="container mx-auto px-6 md:px-8">
@@ -71,6 +71,10 @@ export const activeUserWorkspaceExistenceCheckQuery = graphql(`
}
workspaces(limit: 0) {
totalCount
items {
id
slug
}
}
discoverableWorkspaces {
id
@@ -81,3 +85,30 @@ export const activeUserWorkspaceExistenceCheckQuery = graphql(`
}
}
`)
export const activeUserActiveWorkspaceCheckQuery = graphql(`
query ActiveUserActiveWorkspaceCheck {
activeUser {
id
isProjectsActive
activeWorkspace {
id
slug
}
}
}
`)
export const projectWorkspaceAccessCheckQuery = graphql(`
query projectWorkspaceAccessCheck($projectId: String!) {
project(id: $projectId) {
id
role
workspace {
id
slug
role
}
}
}
`)
@@ -48,6 +48,7 @@ type Documents = {
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.FormUsersSelectItemFragmentDoc,
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": typeof types.HeaderWorkspaceSwitcher_WorkspaceFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n defaultProjectRole\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": typeof types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
@@ -105,8 +106,8 @@ type Documents = {
"\n query ProjectsMoveToWorkspaceDialog {\n activeUser {\n id\n ...ProjectsMoveToWorkspaceDialog_User\n }\n }\n": typeof types.ProjectsMoveToWorkspaceDialogDocument,
"\n fragment ProjectsWorkspaceSelect_Workspace on Workspace {\n id\n role\n name\n logo\n readOnly\n slug\n }\n": typeof types.ProjectsWorkspaceSelect_WorkspaceFragmentDoc,
"\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": typeof types.ProjectsInviteBannerFragmentDoc,
"\n fragment SettingsDialog_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n }\n creationState {\n completed\n }\n }\n": typeof types.SettingsDialog_WorkspaceFragmentDoc,
"\n fragment SettingsDialog_User on User {\n id\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n": typeof types.SettingsDialog_UserFragmentDoc,
"\n fragment SettingsSidebar_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n name\n }\n creationState {\n completed\n }\n }\n": typeof types.SettingsSidebar_WorkspaceFragmentDoc,
"\n fragment SettingsSidebar_User on User {\n id\n workspaces {\n items {\n ...SettingsSidebar_Workspace\n }\n }\n }\n": typeof types.SettingsSidebar_UserFragmentDoc,
"\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n": typeof types.SettingsServerRegionsAddEditDialog_ServerRegionItemFragmentDoc,
"\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n": typeof types.SettingsServerRegionsTable_ServerRegionItemFragmentDoc,
"\n fragment SettingsSharedDeleteUserDialog_Workspace on Workspace {\n id\n plan {\n status\n name\n }\n subscription {\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n }\n": typeof types.SettingsSharedDeleteUserDialog_WorkspaceFragmentDoc,
@@ -157,7 +158,9 @@ type Documents = {
"\n query AuthRegisterPanel($token: String) {\n serverInfo {\n inviteOnly\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n }\n serverInviteByToken(token: $token) {\n id\n email\n }\n }\n": typeof types.AuthRegisterPanelDocument,
"\n query AuthLoginPanelWorkspaceInvite($token: String) {\n workspaceInvite(token: $token) {\n id\n email\n ...AuthWorkspaceInviteHeader_PendingWorkspaceCollaborator\n ...AuthLoginWithEmailBlock_PendingWorkspaceCollaborator\n }\n }\n": typeof types.AuthLoginPanelWorkspaceInviteDocument,
"\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n": typeof types.AuthorizableAppMetadataDocument,
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserWorkspaceExistenceCheckDocument,
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserWorkspaceExistenceCheckDocument,
"\n query ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\n }\n }\n }\n": typeof types.ActiveUserActiveWorkspaceCheckDocument,
"\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n": typeof types.ProjectWorkspaceAccessCheckDocument,
"\n fragment FunctionRunStatusForSummary on AutomateFunctionRun {\n id\n status\n }\n": typeof types.FunctionRunStatusForSummaryFragmentDoc,
"\n fragment TriggeredAutomationsStatusSummary on TriggeredAutomationsStatus {\n id\n automationRuns {\n id\n functionRuns {\n id\n ...FunctionRunStatusForSummary\n }\n }\n }\n": typeof types.TriggeredAutomationsStatusSummaryFragmentDoc,
"\n fragment AutomationRunDetails on AutomateRun {\n id\n status\n functionRuns {\n ...FunctionRunStatusForSummary\n statusMessage\n }\n trigger {\n ... on VersionCreatedTrigger {\n version {\n id\n }\n model {\n id\n }\n }\n }\n createdAt\n updatedAt\n }\n": typeof types.AutomationRunDetailsFragmentDoc,
@@ -204,6 +207,9 @@ type Documents = {
"\n query InviteUserSearch($input: UsersRetrievalInput!) {\n users(input: $input) {\n items {\n id\n name\n avatar\n }\n }\n }\n": typeof types.InviteUserSearchDocument,
"\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": typeof types.CreateNewRegionDocument,
"\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": typeof types.UpdateRegionDocument,
"\n fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\n }\n": typeof types.UseNavigation_WorkspaceFragmentDoc,
"\n mutation SetActiveWorkspace($slug: String, $isProjectsActive: Boolean) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug, isProjectsActive: $isProjectsActive)\n }\n }\n": typeof types.SetActiveWorkspaceDocument,
"\n query HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n": typeof types.HeaderWorkspaceSwitcherDocument,
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": typeof types.ProjectPageTeamInternals_ProjectFragmentDoc,
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": typeof types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": typeof types.ProjectDashboardItemNoModelsFragmentDoc,
@@ -302,7 +308,7 @@ type Documents = {
"\n mutation DeleteWorkspaceDomain($input: WorkspaceDomainDeleteInput!) {\n workspaceMutations {\n deleteDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_Workspace\n }\n }\n }\n": typeof types.DeleteWorkspaceDomainDocument,
"\n mutation SettingsLeaveWorkspace($leaveId: ID!) {\n workspaceMutations {\n leave(id: $leaveId)\n }\n }\n": typeof types.SettingsLeaveWorkspaceDocument,
"\n mutation SettingsBillingCancelCheckoutSession($input: CancelCheckoutSessionInput!) {\n workspaceMutations {\n billing {\n cancelCheckoutSession(input: $input)\n }\n }\n }\n": typeof types.SettingsBillingCancelCheckoutSessionDocument,
"\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n": typeof types.SettingsSidebarDocument,
"\n query SettingsSidebar {\n activeUser {\n ...SettingsSidebar_User\n }\n }\n": typeof types.SettingsSidebarDocument,
"\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n": typeof types.SettingsSidebarAutomateFunctionsDocument,
"\n query SettingsWorkspaceGeneral($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": typeof types.SettingsWorkspaceGeneralDocument,
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": typeof types.SettingsWorkspaceBillingDocument,
@@ -361,7 +367,7 @@ type Documents = {
"\n fragment WorkspaceSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n }\n }\n": typeof types.WorkspaceSecurity_WorkspaceFragmentDoc,
"\n mutation UpdateRole($input: WorkspaceRoleUpdateInput!) {\n workspaceMutations {\n updateRole(input: $input) {\n team {\n items {\n id\n role\n }\n }\n }\n }\n }\n": typeof types.UpdateRoleDocument,
"\n mutation InviteToWorkspace(\n $workspaceId: String!\n $input: [WorkspaceInviteCreateInput!]!\n ) {\n workspaceMutations {\n invites {\n batchCreate(workspaceId: $workspaceId, input: $input) {\n id\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n }\n }\n }\n": typeof types.InviteToWorkspaceDocument,
"\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsDialog_Workspace\n }\n }\n }\n": typeof types.CreateWorkspaceDocument,
"\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsSidebar_Workspace\n }\n }\n }\n": typeof types.CreateWorkspaceDocument,
"\n mutation ProcessWorkspaceInvite($input: WorkspaceInviteUseInput!) {\n workspaceMutations {\n invites {\n use(input: $input)\n }\n }\n }\n": typeof types.ProcessWorkspaceInviteDocument,
"\n mutation SetDefaultWorkspaceRegion($workspaceId: String!, $regionKey: String!) {\n workspaceMutations {\n setDefaultRegion(workspaceId: $workspaceId, regionKey: $regionKey) {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n }\n": typeof types.SetDefaultWorkspaceRegionDocument,
"\n mutation DeleteWorkspaceSsoProvider($workspaceId: String!) {\n workspaceMutations {\n deleteSsoProvider(workspaceId: $workspaceId)\n }\n }\n": typeof types.DeleteWorkspaceSsoProviderDocument,
@@ -441,6 +447,7 @@ const documents: Documents = {
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": types.HeaderWorkspaceSwitcher_WorkspaceFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n defaultProjectRole\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
@@ -498,8 +505,8 @@ const documents: Documents = {
"\n query ProjectsMoveToWorkspaceDialog {\n activeUser {\n id\n ...ProjectsMoveToWorkspaceDialog_User\n }\n }\n": types.ProjectsMoveToWorkspaceDialogDocument,
"\n fragment ProjectsWorkspaceSelect_Workspace on Workspace {\n id\n role\n name\n logo\n readOnly\n slug\n }\n": types.ProjectsWorkspaceSelect_WorkspaceFragmentDoc,
"\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 SettingsDialog_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n }\n creationState {\n completed\n }\n }\n": types.SettingsDialog_WorkspaceFragmentDoc,
"\n fragment SettingsDialog_User on User {\n id\n workspaces {\n items {\n ...SettingsDialog_Workspace\n }\n }\n }\n": types.SettingsDialog_UserFragmentDoc,
"\n fragment SettingsSidebar_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n name\n }\n creationState {\n completed\n }\n }\n": types.SettingsSidebar_WorkspaceFragmentDoc,
"\n fragment SettingsSidebar_User on User {\n id\n workspaces {\n items {\n ...SettingsSidebar_Workspace\n }\n }\n }\n": types.SettingsSidebar_UserFragmentDoc,
"\n fragment SettingsServerRegionsAddEditDialog_ServerRegionItem on ServerRegionItem {\n id\n name\n description\n key\n }\n": types.SettingsServerRegionsAddEditDialog_ServerRegionItemFragmentDoc,
"\n fragment SettingsServerRegionsTable_ServerRegionItem on ServerRegionItem {\n id\n name\n key\n description\n }\n": types.SettingsServerRegionsTable_ServerRegionItemFragmentDoc,
"\n fragment SettingsSharedDeleteUserDialog_Workspace on Workspace {\n id\n plan {\n status\n name\n }\n subscription {\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n }\n": types.SettingsSharedDeleteUserDialog_WorkspaceFragmentDoc,
@@ -550,7 +557,9 @@ const documents: Documents = {
"\n query AuthRegisterPanel($token: String) {\n serverInfo {\n inviteOnly\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n }\n serverInviteByToken(token: $token) {\n id\n email\n }\n }\n": types.AuthRegisterPanelDocument,
"\n query AuthLoginPanelWorkspaceInvite($token: String) {\n workspaceInvite(token: $token) {\n id\n email\n ...AuthWorkspaceInviteHeader_PendingWorkspaceCollaborator\n ...AuthLoginWithEmailBlock_PendingWorkspaceCollaborator\n }\n }\n": types.AuthLoginPanelWorkspaceInviteDocument,
"\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n": types.AuthorizableAppMetadataDocument,
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserWorkspaceExistenceCheckDocument,
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserWorkspaceExistenceCheckDocument,
"\n query ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\n }\n }\n }\n": types.ActiveUserActiveWorkspaceCheckDocument,
"\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n": types.ProjectWorkspaceAccessCheckDocument,
"\n fragment FunctionRunStatusForSummary on AutomateFunctionRun {\n id\n status\n }\n": types.FunctionRunStatusForSummaryFragmentDoc,
"\n fragment TriggeredAutomationsStatusSummary on TriggeredAutomationsStatus {\n id\n automationRuns {\n id\n functionRuns {\n id\n ...FunctionRunStatusForSummary\n }\n }\n }\n": types.TriggeredAutomationsStatusSummaryFragmentDoc,
"\n fragment AutomationRunDetails on AutomateRun {\n id\n status\n functionRuns {\n ...FunctionRunStatusForSummary\n statusMessage\n }\n trigger {\n ... on VersionCreatedTrigger {\n version {\n id\n }\n model {\n id\n }\n }\n }\n createdAt\n updatedAt\n }\n": types.AutomationRunDetailsFragmentDoc,
@@ -597,6 +606,9 @@ const documents: Documents = {
"\n query InviteUserSearch($input: UsersRetrievalInput!) {\n users(input: $input) {\n items {\n id\n name\n avatar\n }\n }\n }\n": types.InviteUserSearchDocument,
"\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": types.CreateNewRegionDocument,
"\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": types.UpdateRegionDocument,
"\n fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\n }\n": types.UseNavigation_WorkspaceFragmentDoc,
"\n mutation SetActiveWorkspace($slug: String, $isProjectsActive: Boolean) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug, isProjectsActive: $isProjectsActive)\n }\n }\n": types.SetActiveWorkspaceDocument,
"\n query HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n": types.HeaderWorkspaceSwitcherDocument,
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": types.ProjectPageTeamInternals_ProjectFragmentDoc,
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": types.ProjectDashboardItemNoModelsFragmentDoc,
@@ -695,7 +707,7 @@ const documents: Documents = {
"\n mutation DeleteWorkspaceDomain($input: WorkspaceDomainDeleteInput!) {\n workspaceMutations {\n deleteDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_Workspace\n }\n }\n }\n": types.DeleteWorkspaceDomainDocument,
"\n mutation SettingsLeaveWorkspace($leaveId: ID!) {\n workspaceMutations {\n leave(id: $leaveId)\n }\n }\n": types.SettingsLeaveWorkspaceDocument,
"\n mutation SettingsBillingCancelCheckoutSession($input: CancelCheckoutSessionInput!) {\n workspaceMutations {\n billing {\n cancelCheckoutSession(input: $input)\n }\n }\n }\n": types.SettingsBillingCancelCheckoutSessionDocument,
"\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n": types.SettingsSidebarDocument,
"\n query SettingsSidebar {\n activeUser {\n ...SettingsSidebar_User\n }\n }\n": types.SettingsSidebarDocument,
"\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n": types.SettingsSidebarAutomateFunctionsDocument,
"\n query SettingsWorkspaceGeneral($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": types.SettingsWorkspaceGeneralDocument,
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
@@ -754,7 +766,7 @@ const documents: Documents = {
"\n fragment WorkspaceSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n }\n }\n": types.WorkspaceSecurity_WorkspaceFragmentDoc,
"\n mutation UpdateRole($input: WorkspaceRoleUpdateInput!) {\n workspaceMutations {\n updateRole(input: $input) {\n team {\n items {\n id\n role\n }\n }\n }\n }\n }\n": types.UpdateRoleDocument,
"\n mutation InviteToWorkspace(\n $workspaceId: String!\n $input: [WorkspaceInviteCreateInput!]!\n ) {\n workspaceMutations {\n invites {\n batchCreate(workspaceId: $workspaceId, input: $input) {\n id\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n }\n }\n }\n": types.InviteToWorkspaceDocument,
"\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsDialog_Workspace\n }\n }\n }\n": types.CreateWorkspaceDocument,
"\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsSidebar_Workspace\n }\n }\n }\n": types.CreateWorkspaceDocument,
"\n mutation ProcessWorkspaceInvite($input: WorkspaceInviteUseInput!) {\n workspaceMutations {\n invites {\n use(input: $input)\n }\n }\n }\n": types.ProcessWorkspaceInviteDocument,
"\n mutation SetDefaultWorkspaceRegion($workspaceId: String!, $regionKey: String!) {\n workspaceMutations {\n setDefaultRegion(workspaceId: $workspaceId, regionKey: $regionKey) {\n id\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n }\n": types.SetDefaultWorkspaceRegionDocument,
"\n mutation DeleteWorkspaceSsoProvider($workspaceId: String!) {\n workspaceMutations {\n deleteSsoProvider(workspaceId: $workspaceId)\n }\n }\n": types.DeleteWorkspaceSsoProviderDocument,
@@ -950,6 +962,10 @@ export function graphql(source: "\n fragment FormUsersSelectItem on LimitedUser
* 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 HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n"): (typeof documents)["\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\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 HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1181,11 +1197,11 @@ export function graphql(source: "\n fragment ProjectsInviteBanner on PendingStr
/**
* 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_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n }\n creationState {\n completed\n }\n }\n"): (typeof documents)["\n fragment SettingsDialog_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n }\n creationState {\n completed\n }\n }\n"];
export function graphql(source: "\n fragment SettingsSidebar_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n name\n }\n creationState {\n completed\n }\n }\n"): (typeof documents)["\n fragment SettingsSidebar_Workspace on Workspace {\n ...SettingsMenu_Workspace\n id\n slug\n role\n name\n logo\n plan {\n status\n name\n }\n creationState {\n completed\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 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"];
export function graphql(source: "\n fragment SettingsSidebar_User on User {\n id\n workspaces {\n items {\n ...SettingsSidebar_Workspace\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsSidebar_User on User {\n id\n workspaces {\n items {\n ...SettingsSidebar_Workspace\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1389,7 +1405,15 @@ export function graphql(source: "\n query AuthorizableAppMetadata($id: String!)
/**
* 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 ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n"];
export function graphql(source: "\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\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 ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\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 projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n"): (typeof documents)["\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1574,6 +1598,18 @@ export function graphql(source: "\n mutation CreateNewRegion($input: CreateServ
* 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 UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\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 fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\n }\n"): (typeof documents)["\n fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\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 SetActiveWorkspace($slug: String, $isProjectsActive: Boolean) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug, isProjectsActive: $isProjectsActive)\n }\n }\n"): (typeof documents)["\n mutation SetActiveWorkspace($slug: String, $isProjectsActive: Boolean) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug, isProjectsActive: $isProjectsActive)\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 HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n"): (typeof documents)["\n query HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1969,7 +2005,7 @@ export function graphql(source: "\n mutation SettingsBillingCancelCheckoutSessi
/**
* 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 SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n"): (typeof documents)["\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n"];
export function graphql(source: "\n query SettingsSidebar {\n activeUser {\n ...SettingsSidebar_User\n }\n }\n"): (typeof documents)["\n query SettingsSidebar {\n activeUser {\n ...SettingsSidebar_User\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2205,7 +2241,7 @@ export function graphql(source: "\n mutation InviteToWorkspace(\n $workspace
/**
* 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 CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsDialog_Workspace\n }\n }\n }\n"): (typeof documents)["\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsDialog_Workspace\n }\n }\n }\n"];
export function graphql(source: "\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsSidebar_Workspace\n }\n }\n }\n"): (typeof documents)["\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n ...SettingsSidebar_Workspace\n }\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
@@ -17,7 +17,6 @@ export const verifyEmailCountdownRoute = '/verify-email?source=registration'
export const serverManagementRoute = '/server-management'
export const connectorsRoute = '/connectors'
export const tutorialsRoute = '/tutorials'
export const downloadManagerUrl = 'https://speckle.systems/download'
export const docsPageUrl = 'https://speckle.guide/'
export const forumPageUrl = 'https://speckle.community/'
export const defaultZapierWebhookUrl =
@@ -0,0 +1,88 @@
import { setActiveWorkspaceMutation } from '~/lib/navigation/graphql/mutations'
import { useMutation, useQuery } from '@vue/apollo-composable'
import { headerWorkspaceSwitcherQuery } from '~/lib/navigation/graphql/queries'
import { graphql } from '~/lib/common/generated/gql'
import type { UseNavigation_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
graphql(`
fragment UseNavigation_Workspace on Workspace {
...HeaderWorkspaceSwitcher_Workspace
id
}
`)
export const useNavigationState = () =>
useState<{
activeWorkspaceSlug: string | null
isProjectsActive: boolean
cachedWorkspaceData: UseNavigation_WorkspaceFragment | null
}>('navigation-state', () => ({
activeWorkspaceSlug: null,
isProjectsActive: false,
cachedWorkspaceData: null
}))
export const useNavigation = () => {
const state = useNavigationState()
const { mutate } = useMutation(setActiveWorkspaceMutation)
const activeWorkspaceSlug = computed({
get: () => state.value.activeWorkspaceSlug,
set: (newVal) => (state.value.activeWorkspaceSlug = newVal)
})
const {
result,
loading: workspaceLoading,
onResult
} = useQuery(
headerWorkspaceSwitcherQuery,
() => ({
slug: activeWorkspaceSlug.value || ''
}),
() => ({
enabled: !!activeWorkspaceSlug.value
})
)
const isProjectsActive = computed({
get: () => state.value.isProjectsActive,
set: (newVal) => (state.value.isProjectsActive = newVal)
})
// Set state and mutate
const mutateActiveWorkspaceSlug = async (newVal: string) => {
state.value.activeWorkspaceSlug = newVal
state.value.isProjectsActive = false
await mutate({ slug: newVal, isProjectsActive: false })
}
const mutateIsProjectsActive = async (isActive: boolean) => {
state.value.isProjectsActive = isActive
state.value.activeWorkspaceSlug = null
state.value.cachedWorkspaceData = null
await mutate({ isProjectsActive: state.value.isProjectsActive, slug: null })
}
// Use the cached data or the current result
const workspaceData = computed(() => {
return result.value?.workspaceBySlug || state.value.cachedWorkspaceData
})
// Save data in the state, the prevent flickering when the component remount in between navigation
onResult((result) => {
const workspace = result.data?.workspaceBySlug
if (workspace) {
state.value.cachedWorkspaceData = workspace
}
})
return {
activeWorkspaceSlug,
isProjectsActive,
mutateActiveWorkspaceSlug,
mutateIsProjectsActive,
workspaceData,
workspaceLoading
}
}
@@ -0,0 +1,9 @@
import { graphql } from '~~/lib/common/generated/gql'
export const setActiveWorkspaceMutation = graphql(`
mutation SetActiveWorkspace($slug: String, $isProjectsActive: Boolean) {
activeUserMutations {
setActiveWorkspace(slug: $slug, isProjectsActive: $isProjectsActive)
}
}
`)
@@ -0,0 +1,9 @@
import { graphql } from '~/lib/common/generated/gql'
export const headerWorkspaceSwitcherQuery = graphql(`
query HeaderWorkspaceSwitcher($slug: String!) {
workspaceBySlug(slug: $slug) {
...HeaderWorkspaceSwitcher_Workspace
}
}
`)
@@ -3,7 +3,7 @@ import { graphql } from '~~/lib/common/generated/gql'
export const settingsSidebarQuery = graphql(`
query SettingsSidebar {
activeUser {
...SettingsDialog_User
...SettingsSidebar_User
}
}
`)
@@ -0,0 +1,24 @@
import { useQuery } from '@vue/apollo-composable'
import { settingsSidebarQuery } from '~/lib/settings/graphql/queries'
export const useUserWorkspaces = () => {
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const { result } = useQuery(settingsSidebarQuery, null, {
enabled: isWorkspacesEnabled.value
})
const workspaces = computed(() =>
result.value?.activeUser
? result.value.activeUser.workspaces.items.filter(
(workspace) => workspace.creationState?.completed !== false
)
: []
)
const hasWorkspaces = computed(() => workspaces.value.length > 0)
return {
workspaces,
hasWorkspaces
}
}
@@ -19,6 +19,7 @@ import { mapMainRoleToGqlWorkspaceRole } from '~/lib/workspaces/helpers/roles'
import { mapServerRoleToGqlServerRole } from '~/lib/common/helpers/roles'
import { Roles } from '@speckle/shared'
import { useMixpanel } from '~/lib/core/composables/mp'
import { useNavigation } from '~/lib/navigation/composables/navigation'
const emptyState: WorkspaceWizardState = {
name: '',
@@ -62,6 +63,7 @@ export const useWorkspacesWizard = () => {
const { mutate: updateWorkspaceCreationState } = useMutation(
updateWorkspaceCreationStateMutation
)
const { mutateActiveWorkspaceSlug } = useNavigation()
const isLoading = computed({
get: () => wizardState.value.isLoading,
@@ -180,6 +182,7 @@ export const useWorkspacesWizard = () => {
} else {
// Keep loading state for a second
await new Promise((resolve) => setTimeout(resolve, 1000))
mutateActiveWorkspaceSlug(wizardState.value.state.slug)
await router.push(workspaceRoute(wizardState.value.state.slug))
await new Promise((resolve) => setTimeout(resolve, 1000))
isLoading.value = false
@@ -189,6 +192,7 @@ export const useWorkspacesWizard = () => {
const finalizeWizard = async (state: WorkspaceWizardState, workspaceId: string) => {
isLoading.value = true
mutateActiveWorkspaceSlug(workspaceId)
if (state.region?.key && state.plan === PaidWorkspacePlans.Business) {
await updateWorkspaceDefaultRegion({
@@ -38,7 +38,7 @@ export const createWorkspaceMutation = graphql(`
workspaceMutations {
create(input: $input) {
id
...SettingsDialog_Workspace
...SettingsSidebar_Workspace
}
}
}
@@ -3,6 +3,7 @@
* 1. Email verification redirect
* 2. Segmentation questions redirect
* 3. Workspace join/create redirect
* 4. Redirect to the correct workspace
*/
import {
@@ -10,13 +11,20 @@ import {
verifyEmailRoute,
onboardingRoute,
workspaceCreateRoute,
workspaceJoinRoute
workspaceJoinRoute,
projectsRoute,
workspaceRoute
} from '~/lib/common/helpers/route'
import { mainServerInfoDataQuery } from '~/lib/core/composables/server'
import { activeUserQuery } from '~~/lib/auth/composables/activeUser'
import { activeUserWorkspaceExistenceCheckQuery } from '~/lib/auth/graphql/queries'
import {
activeUserWorkspaceExistenceCheckQuery,
activeUserActiveWorkspaceCheckQuery,
projectWorkspaceAccessCheckQuery
} from '~/lib/auth/graphql/queries'
import { useApolloClientFromNuxt } from '~~/lib/common/composables/graphql'
import { convertThrowIntoFetchResult } from '~~/lib/common/helpers/graphql'
import { useNavigation } from '~/lib/navigation/composables/navigation'
export default defineNuxtRouteMiddleware(async (to) => {
const isAuthPage = to.path.startsWith('/authn/')
@@ -24,6 +32,15 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (isAuthPage || isSSOPath) return
const client = useApolloClientFromNuxt()
const {
activeWorkspaceSlug,
isProjectsActive,
mutateActiveWorkspaceSlug,
mutateIsProjectsActive
} = useNavigation()
// Track if this is the initial load
const isAppInitialized = useState<boolean>('app-initialized', () => false)
// Fetch required data
const { data: serverInfoData } = await client
@@ -67,38 +84,41 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (!isSegmentationFinished && !isGoingToSegmentation) {
return navigateTo(onboardingRoute)
}
if (isGoingToSegmentation && isSegmentationFinished) {
return navigateTo(homeRoute)
}
// Don't run any other checks if the user has not finished the onboarding process
if (!isSegmentationFinished) return
// 3. Workspace join/create redirect
// Everything past this point is only relevant for workspace enabled instances
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
if (!isWorkspacesEnabled.value || !isWorkspaceNewPlansEnabled.value) return
// This query will throw an error if workspaces FF are not enabled, so check for that first
const { data: workspaceData } = await client
const { data: workspaceExistenceData } = await client
.query({
query: activeUserWorkspaceExistenceCheckQuery
})
.catch(convertThrowIntoFetchResult)
const isMemberOfWorkspace =
(workspaceData?.activeUser?.workspaces?.totalCount ?? 0) > 0
const workspaces = workspaceExistenceData?.activeUser?.workspaces?.items ?? []
const hasWorkspaces = workspaces.length > 0
const hasDiscoverableWorkspaces =
(workspaceData?.activeUser?.discoverableWorkspaces?.length ?? 0) > 0 ||
(workspaceData?.activeUser?.workspaceJoinRequests?.totalCount ?? 0) > 0
(workspaceExistenceData?.activeUser?.discoverableWorkspaces?.length ?? 0) > 0 ||
(workspaceExistenceData?.activeUser?.workspaceJoinRequests?.totalCount ?? 0) > 0
// If user has existing projects, we consider these legacy projects, and don't block app access yet
const hasLegacyProjects = (workspaceData?.activeUser?.versions?.totalCount ?? 0) > 0
const hasLegacyProjects =
(workspaceExistenceData?.activeUser?.versions?.totalCount ?? 0) > 0
const isGoingToJoinWorkspace = to.path === workspaceJoinRoute
const isGoingToCreateWorkspace = to.path === workspaceCreateRoute()
// If user has discoverable workspaces, or has pending requests, go to join. Otherwise, we go to create.
if (!isMemberOfWorkspace && !hasLegacyProjects) {
if (!hasWorkspaces && !hasLegacyProjects) {
if (
hasDiscoverableWorkspaces &&
!isGoingToJoinWorkspace &&
@@ -110,4 +130,88 @@ export default defineNuxtRouteMiddleware(async (to) => {
return navigateTo(workspaceCreateRoute())
}
}
// 4. Redirect to the correct workspace
// If there is an active workspace slug or legacy projects if active, we don't need to do anything
if (activeWorkspaceSlug.value || isProjectsActive.value) return
// Skip workspace/project navigation logic if it's not the initial load
if (isAppInitialized.value) return
// Mark as initialized for future navigations
isAppInitialized.value = true
const { data: navigationCheckData } = await client
.query({
query: activeUserActiveWorkspaceCheckQuery
})
.catch(convertThrowIntoFetchResult)
const activeUserIsProjectsActive = navigationCheckData?.activeUser?.isProjectsActive
const activeUserActiveWorkspaceSlug =
navigationCheckData?.activeUser?.activeWorkspace?.slug
const belongsToWorkspace = (slug: string) =>
workspaces.find((workspace) => workspace.slug === slug)
// 4.2 If going to legacy projects, set it active
if (to.path === projectsRoute) {
if (hasLegacyProjects) {
mutateIsProjectsActive(true)
} else {
if (hasWorkspaces) {
mutateActiveWorkspaceSlug(workspaces[0].slug)
navigateTo(workspaceRoute(workspaces[0].slug))
}
}
return
}
// 4.3 If going to workspace, set it active
if (to.path.startsWith('/workspaces/')) {
const slug = to.params.slug as string
if (slug && belongsToWorkspace(slug)) {
mutateActiveWorkspaceSlug(slug)
}
return
}
// 4.4 If going to a project route, check access
if (to.path.startsWith('/projects/')) {
const { data: projectCheckData } = await client
.query({
query: projectWorkspaceAccessCheckQuery,
variables: {
projectId: to.params.id as string
}
})
.catch(convertThrowIntoFetchResult)
const project = projectCheckData?.project
if (project) {
// If the project is part of a workspace set it as active if the user has access
if (project.workspace && project.workspace.role) {
mutateActiveWorkspaceSlug(project.workspace.slug)
} else if (project.role) {
// Else set projects active
mutateIsProjectsActive(true)
}
}
return
}
// 4.5 For all other routes, check for previous state
if (activeUserActiveWorkspaceSlug) {
activeWorkspaceSlug.value = activeUserActiveWorkspaceSlug
} else if (activeUserIsProjectsActive) {
isProjectsActive.value = true
}
if (to.path === homeRoute) {
if (activeUserActiveWorkspaceSlug) {
return navigateTo(workspaceRoute(activeUserActiveWorkspaceSlug))
} else if (activeUserIsProjectsActive) {
return navigateTo(projectsRoute)
}
}
})
@@ -30,7 +30,9 @@
<CommonBadge
v-if="tag"
rounded
:color-classes="disabled ? 'text-foreground-2 bg-primary-muted' : undefined"
:color-classes="
colorClasses ?? (disabled ? 'text-foreground-2 bg-primary-muted' : undefined)
"
>
{{ tag }}
</CommonBadge>
@@ -72,6 +74,7 @@ const props = defineProps<{
active?: boolean
tooltipText?: string
extraPadding?: boolean
colorClasses?: string
}>()
const isOpen = ref(true)