Feat: New navigation (#4179)
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user