Merge remote-tracking branch 'origin/main' into adam/add-ol2-options
This commit is contained in:
@@ -1,20 +1,14 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="open" max-width="sm">
|
||||
<template #header>Share dashboard</template>
|
||||
<h4 class="text-body-xs font-medium text-foreground mb-1">Dashboard URL</h4>
|
||||
<FormClipboardInput :value="dashboardUrl" />
|
||||
|
||||
<div v-if="canCreateToken">
|
||||
<hr class="my-4 border-outline-3" />
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-body-xs font-medium text-foreground">Enable public access</p>
|
||||
<p class="text-body-2xs text-foreground-2">Anyone with the link can view</p>
|
||||
</div>
|
||||
<FormSwitch v-model="enablePublicUrl" name="isPublic" :show-label="false" />
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-body-xs font-medium text-foreground">Enable public access</p>
|
||||
<p class="text-body-2xs text-foreground-2">Anyone with the link can view</p>
|
||||
</div>
|
||||
<FormClipboardInput v-if="enablePublicUrl" class="mt-3" :value="shareUrl" />
|
||||
<FormSwitch v-model="enablePublicUrl" name="isPublic" :show-label="false" />
|
||||
</div>
|
||||
<FormClipboardInput v-if="enablePublicUrl" class="mt-3" :value="shareUrl" />
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
@@ -23,6 +17,11 @@ import { dashboardRoute } from '~~/lib/common/helpers/route'
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
|
||||
import {
|
||||
convertThrowIntoFetchResult,
|
||||
getFirstErrorMessage
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
|
||||
const dashboardsDialogSharePermissionsQuery = graphql(`
|
||||
query DashboardsSharDialogPermissions($id: String!) {
|
||||
@@ -33,11 +32,6 @@ const dashboardsDialogSharePermissionsQuery = graphql(`
|
||||
content
|
||||
revoked
|
||||
}
|
||||
permissions {
|
||||
canCreateToken {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -91,6 +85,7 @@ const { result, refetch } = useQuery(dashboardsDialogSharePermissionsQuery, () =
|
||||
const { mutate: createToken } = useMutation(dashboardsDialogShareTokenMutation)
|
||||
const { mutate: disableToken } = useMutation(dashboardsDialogShareDisableTokenMutation)
|
||||
const { mutate: enableToken } = useMutation(dashboardsDialogShareEnableTokenMutation)
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const isRevoked = computed(() => result.value?.dashboard?.shareLink?.revoked)
|
||||
const shareLink = computed(() => result.value?.dashboard?.shareLink)
|
||||
@@ -105,23 +100,12 @@ const shareUrl = computed(() => {
|
||||
|
||||
return url.toString()
|
||||
})
|
||||
const canCreateToken = computed(
|
||||
() => result.value?.dashboard?.permissions?.canCreateToken?.authorized
|
||||
)
|
||||
const enablePublicUrl = computed({
|
||||
get: () => !isRevoked.value && !!shareLink.value?.id,
|
||||
set: (value: boolean) => {
|
||||
onEnablePublicUrl(value)
|
||||
}
|
||||
})
|
||||
const dashboardUrl = computed(() => {
|
||||
if (!props.workspaceSlug || !props.dashboardId) return ''
|
||||
|
||||
return new URL(
|
||||
dashboardRoute(props.workspaceSlug, props.dashboardId),
|
||||
window.location.toString()
|
||||
).toString()
|
||||
})
|
||||
|
||||
const onEnablePublicUrl = async (value: boolean) => {
|
||||
if (!props.dashboardId) return
|
||||
@@ -129,7 +113,18 @@ const onEnablePublicUrl = async (value: boolean) => {
|
||||
if (value) {
|
||||
// If enabling and no share link exists, create one first
|
||||
if (!shareLink.value?.id) {
|
||||
await createToken({ dashboardId: props.dashboardId })
|
||||
const result = await createToken({ dashboardId: props.dashboardId }).catch(
|
||||
convertThrowIntoFetchResult
|
||||
)
|
||||
|
||||
if (!result?.data?.dashboardMutations.share.id) {
|
||||
const errMsg = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to enable public access',
|
||||
description: errMsg
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the share link
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
color="outline"
|
||||
class="hidden sm:flex"
|
||||
size="sm"
|
||||
:disabled="!canCreateToken"
|
||||
@click="shareDialogOpen = true"
|
||||
>
|
||||
Share
|
||||
@@ -20,34 +19,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
|
||||
const dashboardsSharePermissionsQuery = graphql(`
|
||||
query DashboardsSharePermissions($id: String!) {
|
||||
dashboard(id: $id) {
|
||||
id
|
||||
permissions {
|
||||
canCreateToken {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
id: MaybeNullOrUndefined<string>
|
||||
workspaceSlug: MaybeNullOrUndefined<string>
|
||||
}>()
|
||||
|
||||
const { result } = useQuery(dashboardsSharePermissionsQuery, {
|
||||
id: props.id as string
|
||||
})
|
||||
|
||||
const canCreateToken = computed(
|
||||
() => result.value?.dashboard?.permissions?.canCreateToken?.authorized
|
||||
)
|
||||
|
||||
const shareDialogOpen = ref(false)
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{{ presentation?.title }}
|
||||
</h1>
|
||||
|
||||
<LayoutMenu
|
||||
<!-- <LayoutMenu
|
||||
v-model:open="showMenu"
|
||||
class="hidden md:block"
|
||||
:items="menuItems"
|
||||
@@ -26,14 +26,14 @@
|
||||
<PresentationFloatingPanelButton @click="showMenu = !showMenu">
|
||||
<LucideEllipsis class="size-4" />
|
||||
</PresentationFloatingPanelButton>
|
||||
</LayoutMenu>
|
||||
</LayoutMenu> -->
|
||||
</div>
|
||||
</PresentationFloatingPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LucideArrowLeftToLine, LucidePanelLeft, LucideEllipsis } from 'lucide-vue-next'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
import { LucideArrowLeftToLine, LucidePanelLeft } from 'lucide-vue-next'
|
||||
// import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
@@ -44,9 +44,9 @@ graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
enum MenuItems {
|
||||
OpenInViewer = 'open-in-viewer'
|
||||
}
|
||||
// enum MenuItems {
|
||||
// OpenInViewer = 'open-in-viewer'
|
||||
// }
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggleSidebar'): void
|
||||
@@ -57,25 +57,25 @@ const isSidebarOpen = defineModel<boolean>('is-sidebar-open')
|
||||
const {
|
||||
response: { presentation }
|
||||
} = useInjectedPresentationState()
|
||||
const menuId = useId()
|
||||
// const menuId = useId()
|
||||
|
||||
const showMenu = ref(false)
|
||||
// const showMenu = ref(false)
|
||||
|
||||
const menuItems = computed<LayoutMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
title: 'Open in viewer',
|
||||
id: MenuItems.OpenInViewer
|
||||
}
|
||||
]
|
||||
])
|
||||
// const menuItems = computed<LayoutMenuItem[][]>(() => [
|
||||
// [
|
||||
// {
|
||||
// title: 'Open in viewer',
|
||||
// id: MenuItems.OpenInViewer
|
||||
// }
|
||||
// ]
|
||||
// ])
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem }) => {
|
||||
const { item } = params
|
||||
// const onActionChosen = (params: { item: LayoutMenuItem }) => {
|
||||
// const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case MenuItems.OpenInViewer:
|
||||
// Will be added soon
|
||||
}
|
||||
}
|
||||
// switch (item.id) {
|
||||
// case MenuItems.OpenInViewer:
|
||||
// // Will be added soon
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<template>
|
||||
<aside
|
||||
class="bg-foundation h-48 md:h-screen w-full md:w-64 xl:w-80 border-t md:border-t-0 md:border-l border-outline-3 py-5 px-4"
|
||||
class="bg-foundation h-48 md:h-dvh w-full md:w-64 xl:w-80 border-t md:border-t-0 md:border-l border-outline-3 py-5 px-4"
|
||||
>
|
||||
<div class="hidden md:flex items-center justify-end space-x-0.5">
|
||||
<FormButton
|
||||
v-if="canUpdate"
|
||||
:icon-left="LucidePencilLine"
|
||||
color="subtle"
|
||||
hide-text
|
||||
@click="isSlideEditDialogOpen = true"
|
||||
/>
|
||||
<div
|
||||
v-tippy="
|
||||
canUpdateSlide ? undefined : 'You do not have permission to edit this slide'
|
||||
"
|
||||
>
|
||||
<FormButton
|
||||
v-if="canUpdate"
|
||||
:disabled="!canUpdateSlide"
|
||||
:icon-left="LucidePencilLine"
|
||||
color="subtle"
|
||||
hide-text
|
||||
@click="isSlideEditDialogOpen = true"
|
||||
/>
|
||||
</div>
|
||||
<FormButton
|
||||
:icon-left="LucideX"
|
||||
color="subtle"
|
||||
@@ -43,6 +50,7 @@
|
||||
<PresentationSlideEditDialog
|
||||
v-model:open="isSlideEditDialogOpen"
|
||||
:slide="currentSlide"
|
||||
:workspace-id="workspace?.id"
|
||||
/>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -71,17 +79,25 @@ graphql(`
|
||||
...PresentationSlideEditDialog_SavedView
|
||||
name
|
||||
description
|
||||
permissions {
|
||||
canUpdate {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const {
|
||||
ui: { slide: currentSlide },
|
||||
response: { presentation }
|
||||
response: { presentation, workspace }
|
||||
} = useInjectedPresentationState()
|
||||
|
||||
const isSlideEditDialogOpen = ref(false)
|
||||
|
||||
const canUpdate = computed(() => {
|
||||
return presentation.value?.permissions?.canUpdate
|
||||
return presentation.value?.permissions?.canUpdate?.authorized
|
||||
})
|
||||
const canUpdateSlide = computed(() => {
|
||||
return currentSlide.value?.permissions?.canUpdate.authorized
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,15 +5,27 @@
|
||||
</div>
|
||||
|
||||
<aside
|
||||
class="relative z-20 bg-foundation h-screen w-52 md:w-60 border-r border-outline-3 pt-3"
|
||||
class="relative z-20 bg-foundation h-dvh w-52 md:w-60 border-r border-outline-3 pt-3"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<section class="flex-shrink-0 flex items-center gap-2.5 px-3">
|
||||
<WorkspaceAvatar :name="workspace?.name" :logo="workspace?.logo" />
|
||||
<p class="text-body-xs text-foreground">
|
||||
{{ workspace?.name }}
|
||||
</p>
|
||||
<UserAvatar size="sm" class="ml-auto flex-shrink-0" :user="activeUser" />
|
||||
<section class="flex-shrink-0 flex items-center gap-3 px-3">
|
||||
<NuxtLink
|
||||
class="flex items-center gap-2 min-w-0 flex-1"
|
||||
:to="workspaceRoute(workspace?.slug)"
|
||||
>
|
||||
<WorkspaceAvatar :name="workspace?.name" :logo="workspace?.logo" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-body-xs text-foreground truncate">
|
||||
{{ workspace?.name }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<UserAvatar
|
||||
v-if="isLoggedIn"
|
||||
size="sm"
|
||||
class="ml-auto flex-shrink-0"
|
||||
:user="activeUser"
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
class="flex-1 flex justify-center simple-scrollbar overflow-y-auto mt-3 pb-3 px-3"
|
||||
@@ -34,7 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { loginRoute, registerRoute } from '~~/lib/common/helpers/route'
|
||||
import { loginRoute, registerRoute, workspaceRoute } from '~~/lib/common/helpers/route'
|
||||
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
@@ -43,6 +55,7 @@ graphql(`
|
||||
id
|
||||
name
|
||||
logo
|
||||
slug
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="h-screen w-screen flex flex-col md:flex-row relative">
|
||||
<div class="h-dvh w-screen flex flex-col md:flex-row relative">
|
||||
<PresentationHeader
|
||||
v-if="!hideUi"
|
||||
v-model:is-sidebar-open="isLeftSidebarOpen"
|
||||
@@ -21,8 +21,10 @@
|
||||
/>
|
||||
|
||||
<PresentationSlideIndicator
|
||||
v-if="!isViewerLoading"
|
||||
:show-slide-list="!isLeftSidebarOpen"
|
||||
class="absolute top-1/2 translate-y-[calc(-50%+25px)] z-20"
|
||||
:class="[isLeftSidebarOpen ? 'lg:left-[15.75rem] hidden md:block' : 'left-4']"
|
||||
:class="[isLeftSidebarOpen ? 'lg:left-[14.75rem] hidden md:block' : 'left-0']"
|
||||
/>
|
||||
|
||||
<PresentationSpeckleLogo
|
||||
@@ -40,6 +42,8 @@
|
||||
:is="presentation ? ViewerWrapper : 'div'"
|
||||
:group="presentation"
|
||||
class="h-full w-full object-cover"
|
||||
@loading-change="onLoadingChange"
|
||||
@progress-change="onProgressChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +54,7 @@
|
||||
/>
|
||||
|
||||
<PresentationControls
|
||||
v-if="!hideUi"
|
||||
:hide-ui="hideUi"
|
||||
class="absolute left-4 md:left-1/2 md:-translate-x-1/2"
|
||||
:class="[
|
||||
isInfoSidebarOpen ? 'bottom-52 md:bottom-4' : 'bottom-4',
|
||||
@@ -63,25 +67,55 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { useEventListener, useBreakpoints } from '@vueuse/core'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
|
||||
const {
|
||||
response: { presentation }
|
||||
response: { presentation, workspace }
|
||||
} = useInjectedPresentationState()
|
||||
const mixpanel = useMixpanel()
|
||||
const isMobile = useBreakpoints(TailwindBreakpoints).smaller('sm')
|
||||
|
||||
const isInfoSidebarOpen = ref(true)
|
||||
const isLeftSidebarOpen = ref(true)
|
||||
const hideUi = ref(false)
|
||||
const isInfoSidebarOpen = ref(false)
|
||||
const isLeftSidebarOpen = ref(false)
|
||||
const hideUi = ref(true)
|
||||
const isViewerLoading = ref(true)
|
||||
const viewerProgress = ref(0)
|
||||
|
||||
const ViewerWrapper = resolveComponent('PresentationViewerWrapper')
|
||||
|
||||
const onLoadingChange = (loading: boolean) => {
|
||||
isViewerLoading.value = loading
|
||||
|
||||
if (!loading) {
|
||||
hideUi.value = false
|
||||
|
||||
isLeftSidebarOpen.value = !isMobile.value
|
||||
isInfoSidebarOpen.value = !isMobile.value
|
||||
}
|
||||
}
|
||||
|
||||
const onProgressChange = (progress: number) => {
|
||||
viewerProgress.value = progress
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'i' || event.key === 'I') {
|
||||
hideUi.value = !hideUi.value
|
||||
isLeftSidebarOpen.value = false
|
||||
isInfoSidebarOpen.value = false
|
||||
isLeftSidebarOpen.value = !hideUi.value
|
||||
isInfoSidebarOpen.value = !hideUi.value
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener('keydown', handleKeydown)
|
||||
|
||||
onMounted(() => {
|
||||
mixpanel.track('Presentation Viewed', {
|
||||
// eslint-disable-next-line camelcase
|
||||
presentation_id: presentation.value?.id,
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspace.value?.id
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -47,6 +47,7 @@ graphql(`
|
||||
|
||||
const props = defineProps<{
|
||||
slide: MaybeNullOrUndefined<PresentationSlideEditDialog_SavedViewFragment>
|
||||
workspaceId: MaybeNullOrUndefined<string>
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
@@ -61,10 +62,13 @@ const onSubmit = handleSubmit(async () => {
|
||||
if (!props.slide?.id) return
|
||||
|
||||
await updateSlide({
|
||||
id: props.slide.id,
|
||||
projectId: props.slide.projectId,
|
||||
name: name.value,
|
||||
description: description.value
|
||||
input: {
|
||||
id: props.slide.id,
|
||||
projectId: props.slide.projectId,
|
||||
name: name.value,
|
||||
description: description.value
|
||||
},
|
||||
workspaceId: props.workspaceId
|
||||
})
|
||||
|
||||
open.value = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="p-4 pl-0 absolute">
|
||||
<div class="p-4 absolute group">
|
||||
<ul class="flex flex-col space-y-2">
|
||||
<li v-for="slide in visibleSlides" :key="slide.id">
|
||||
<div
|
||||
@@ -9,11 +9,12 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- <div
|
||||
class="hidden md:flex absolute top-[calc(50%+25px)] -translate-y-1/2 max-h-[75vh] overflow-y-auto w-56 simple-scrollbar bg-foundation border border-outline-3 rounded-xl p-3 shadow-md"
|
||||
<div
|
||||
v-if="showSlideList"
|
||||
class="absolute top-[calc(50%+25px)] -translate-y-1/2 max-h-[75vh] overflow-y-auto w-56 simple-scrollbar bg-foundation border border-outline-3 rounded-xl p-3 shadow-md transition-all duration-300 ease-out opacity-0 invisible group-hover:opacity-100 group-hover:visible -translate-x-5 group-hover:translate-x-0"
|
||||
>
|
||||
<PresentationSlideList class="w-full" hide-title />
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,4 +25,8 @@ const {
|
||||
ui: { slide: currentView },
|
||||
response: { visibleSlides }
|
||||
} = useInjectedPresentationState()
|
||||
|
||||
defineProps<{
|
||||
showSlideList?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center rounded-xl bg-foundation border border-outline-3 shadow-md overflow-hidden divide-x divide-outline-3"
|
||||
:class="{ hidden: hideUi }"
|
||||
>
|
||||
<PresentationControlsButton
|
||||
:icon="LucideChevronLeft"
|
||||
@@ -22,6 +23,10 @@ import { useInjectedPresentationState } from '~/lib/presentations/composables/se
|
||||
import { clamp } from 'lodash-es'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
defineProps<{
|
||||
hideUi?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
ui: { slideIdx: currentVisibleIndex, slideCount },
|
||||
viewer: { resetView }
|
||||
@@ -38,15 +43,32 @@ const onNext = () => {
|
||||
currentVisibleIndex.value = clamp(currentVisibleIndex.value + 1, 0, slideCount.value)
|
||||
}
|
||||
|
||||
// TBD
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowLeft' && !disablePrevious.value) {
|
||||
onPrevious()
|
||||
}
|
||||
if (event.key === 'ArrowRight' && !disableNext.value) {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
// Prevent viewer from moving when using arrow keys
|
||||
useEventListener(
|
||||
'keydown',
|
||||
(event) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.stopImmediatePropagation()
|
||||
if (disablePrevious.value) return
|
||||
onPrevious()
|
||||
}
|
||||
|
||||
useEventListener('keydown', handleKeydown)
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.stopImmediatePropagation()
|
||||
if (disableNext.value) return
|
||||
onNext()
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<button
|
||||
class="size-8 flex items-center justify-center bg-foundation rounded-xl hover:bg-info-lighter hover:text-primary-focus"
|
||||
:class="{ 'bg-info-lighter text-primary-focus': isActive }"
|
||||
class="size-8 flex items-center justify-center bg-foundation rounded-xl hover:bg-info-lighter hover:text-primary-focus dark:hover:text-foreground-on-primary"
|
||||
:class="{
|
||||
'bg-info-lighter text-primary-focus dark:text-foreground-on-primary': isActive
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<li :class="{ 'pb-4': hideTitle }">
|
||||
<li :class="{ 'pb-1 last:pb-0': hideTitle }">
|
||||
<button
|
||||
class="bg-foundation-page rounded-xl overflow-hidden border border-outline-3 transition-all duration-200 hover:!border-outline-4"
|
||||
:class="[isCurrentSlide ? '!border-outline-5' : '']"
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
<template>
|
||||
<div class="presentation-viewer-setup h-full">
|
||||
<ViewerCoreSetup viewer-host-classes="h-full" />
|
||||
<ViewerCoreSetup
|
||||
viewer-host-classes="h-full"
|
||||
:disable-selection="disableSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
|
||||
defineProps<{
|
||||
disableSelection?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loading-change', loading: boolean): void
|
||||
(e: 'progress-change', progress: number): void
|
||||
}>()
|
||||
|
||||
const {
|
||||
ui: { loading, loadProgress }
|
||||
} = useInjectedViewerState()
|
||||
|
||||
watch(loading, (newLoading) => {
|
||||
emit('loading-change', newLoading)
|
||||
})
|
||||
|
||||
watch(loadProgress, (newProgress) => {
|
||||
emit('progress-change', newProgress)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<ViewerStateSetup :init-params="initParams">
|
||||
<PresentationViewerSetup />
|
||||
<PresentationViewerSetup
|
||||
@loading-change="onLoadingChange"
|
||||
@progress-change="onProgressChange"
|
||||
/>
|
||||
</ViewerStateSetup>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -66,6 +69,19 @@ const savedViewId = computed({
|
||||
|
||||
const loadOriginal = ref(false)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loading-change', loading: boolean): void
|
||||
(e: 'progress-change', progress: number): void
|
||||
}>()
|
||||
|
||||
const onLoadingChange = (loading: boolean) => {
|
||||
emit('loading-change', loading)
|
||||
}
|
||||
|
||||
const onProgressChange = (progress: number) => {
|
||||
emit('progress-change', progress)
|
||||
}
|
||||
|
||||
const initParams = computed(
|
||||
(): UseSetupViewerParams => ({
|
||||
projectId,
|
||||
|
||||
@@ -2,20 +2,27 @@
|
||||
<template>
|
||||
<div
|
||||
ref="resizableElement"
|
||||
class="relative sm:absolute z-10 right-0 overflow-hidden w-screen bottom-0 sm:bottom-auto sm:top-[3.5rem] lg:top-[3rem] sm:right-2 lg:right-0 h-[40dvh] sm:h-[calc(100dvh-8rem)] lg:h-[calc(100dvh-3rem)] sm:max-w-[264px]"
|
||||
class="relative sm:absolute z-10 right-0 overflow-hidden w-screen bottom-0 sm:bottom-auto sm:right-2 h-[40dvh] sm:max-w-[276px]"
|
||||
:style="isLgOrLarger ? { maxWidth: width + 'px' } : {}"
|
||||
:class="[open ? '' : 'pointer-events-none']"
|
||||
:class="[
|
||||
open ? '' : 'pointer-events-none',
|
||||
isEmbedEnabled
|
||||
? 'sm:top-2 sm:h-[calc(100dvh-8rem)]'
|
||||
: 'sm:top-[3.5rem] lg:h-[calc(100dvh-3rem)] lg:top-[3rem] lg:right-0 sm:h-[calc(100dvh-8rem)]'
|
||||
]"
|
||||
>
|
||||
<div class="flex h-full" :class="open ? '' : 'sm:translate-x-[100%]'">
|
||||
<!-- Resize Handle -->
|
||||
<div
|
||||
v-if="!isEmbedEnabled"
|
||||
ref="resizeHandle"
|
||||
class="absolute h-full max-h-[calc(100dvh-3rem)] w-4 transition border-l sm:rounded-lg lg:rounded-none hover:border-l-[2px] border-outline-2 hover:border-primary hidden lg:flex items-center cursor-ew-resize z-30"
|
||||
@mousedown="startResizing"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-col w-full h-full relative z-20 overflow-hidden sm:rounded-lg lg:rounded-none border-l border-t sm:border lg:border-0 lg:border-l border-outline-2 bg-foundation"
|
||||
class="flex flex-col w-full h-full relative z-20 overflow-hidden sm:rounded-lg border-l border-t sm:border border-outline-2 bg-foundation"
|
||||
:class="!isEmbedEnabled ? 'lg:rounded-none lg:border-0 lg:border-l' : ''"
|
||||
>
|
||||
<div
|
||||
class="h-10 pl-4 pr-2 flex items-center justify-between border-b border-outline-2"
|
||||
@@ -39,6 +46,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useEventListener, useBreakpoints } from '@vueuse/core'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
@@ -52,12 +60,13 @@ const emit = defineEmits<{
|
||||
const resizableElement = ref(null)
|
||||
const resizeHandle = ref(null)
|
||||
const isResizing = ref(false)
|
||||
const width = ref(280)
|
||||
const width = ref(276)
|
||||
let startWidth = 0
|
||||
let startX = 0
|
||||
|
||||
const breakpoints = useBreakpoints(TailwindBreakpoints)
|
||||
const isLgOrLarger = breakpoints.greaterOrEqual('lg')
|
||||
const { isEnabled: isEmbedEnabled } = useEmbed()
|
||||
|
||||
const startResizing = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
||||
<template>
|
||||
<aside
|
||||
class="absolute left-2 lg:left-0 z-50 flex rounded-lg border border-outline-2 bg-foundation px-1 overflow-visible lg:h-full focus-visible:outline-none"
|
||||
class="absolute left-2 z-50 flex rounded-lg border border-outline-2 bg-foundation px-1 overflow-visible focus-visible:outline-none"
|
||||
:class="[
|
||||
isEmbedEnabled
|
||||
? 'top-[0.5rem]'
|
||||
: 'top-[3.5rem] lg:top-[3rem] lg:rounded-none lg:px-2 lg:max-h-[calc(100dvh-3rem)] lg:border-l-0 lg:border-t-0 lg:border-b-0',
|
||||
: 'top-[3.5rem] lg:top-[3rem] lg:rounded-none lg:px-2 lg:max-h-[calc(100dvh-3rem)] lg:border-l-0 lg:border-t-0 lg:border-b-0 lg:h-full lg:left-0',
|
||||
hasActivePanel && 'h-full max-h-[calc(100dvh-8rem)] rounded-r-none'
|
||||
]"
|
||||
>
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
<!-- Resize handle -->
|
||||
<div
|
||||
v-if="activePanel !== 'none'"
|
||||
v-if="activePanel !== 'none' && !isEmbedEnabled"
|
||||
ref="resizeHandle"
|
||||
class="absolute h-full max-h-[calc(100dvh-3rem)] w-4 transition border-l hover:border-l-[2px] border-outline-2 hover:border-primary hidden lg:flex items-center cursor-ew-resize z-30"
|
||||
:style="`left:${width + 52}px;`"
|
||||
@@ -289,7 +289,7 @@ const throttledHandleMouseMove = useThrottleFn((event: MouseEvent) => {
|
||||
)
|
||||
panelExtensionWidth.value = newWidth
|
||||
}
|
||||
}, 150)
|
||||
}, 50)
|
||||
|
||||
if (import.meta.client) {
|
||||
useResizeObserver(scrollableControlsContainer, (entries) => {
|
||||
|
||||
@@ -95,10 +95,6 @@ import { useFilterUtilities } from '~/lib/viewer/composables/filtering/filtering
|
||||
import { useFilterColoringHelpers } from '~/lib/viewer/composables/filtering/coloringHelpers'
|
||||
import type { FilterData } from '~/lib/viewer/helpers/filters/types'
|
||||
import { FilterType } from '~/lib/viewer/helpers/filters/types'
|
||||
import {
|
||||
useHighlightedObjectsUtilities,
|
||||
useSelectionUtilities
|
||||
} from '~/lib/viewer/composables/ui'
|
||||
|
||||
const props = defineProps<{
|
||||
filter: FilterData
|
||||
@@ -112,8 +108,6 @@ const { removeActiveFilter, toggleFilterApplied, getPropertyName, filters } =
|
||||
useFilterUtilities()
|
||||
|
||||
const { toggleColorFilter } = useFilterColoringHelpers()
|
||||
const { clearHighlightedObjects } = useHighlightedObjectsUtilities()
|
||||
const { clearSelection } = useSelectionUtilities()
|
||||
|
||||
const emit = defineEmits<{
|
||||
swapProperty: [filterId: string]
|
||||
@@ -124,8 +118,6 @@ const isColoringActive = computed(() => {
|
||||
})
|
||||
|
||||
const removeFilter = () => {
|
||||
clearHighlightedObjects()
|
||||
clearSelection()
|
||||
removeActiveFilter(props.filter.id)
|
||||
}
|
||||
|
||||
@@ -134,8 +126,6 @@ const toggleVisibility = () => {
|
||||
}
|
||||
|
||||
const toggleColors = () => {
|
||||
clearHighlightedObjects()
|
||||
clearSelection()
|
||||
toggleColorFilter(props.filter.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusin="highlightObject"
|
||||
@focusout="unhighlightObject"
|
||||
@click="selectObject"
|
||||
@click="handleClick"
|
||||
@dblclick="zoomToModel"
|
||||
@keydown.enter="selectObject"
|
||||
@keydown.enter="handleClick"
|
||||
>
|
||||
<ViewerExpansionTriangle
|
||||
class="h-8"
|
||||
@@ -104,7 +104,8 @@ import type { Get } from 'type-fest'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
import {
|
||||
useHighlightedObjectsUtilities,
|
||||
useCameraUtilities
|
||||
useCameraUtilities,
|
||||
useSelectionUtilities
|
||||
} from '~~/lib/viewer/composables/ui'
|
||||
import { useFilterUtilities } from '~/lib/viewer/composables/filtering/filtering'
|
||||
import {
|
||||
@@ -139,6 +140,8 @@ const { hideObjects, showObjects, isolateObjects, unIsolateObjects } =
|
||||
const { zoom } = useCameraUtilities()
|
||||
const { items } = useInjectedViewerRequestedResources()
|
||||
const { resourceItems } = useInjectedViewerLoadedResources()
|
||||
const { addToSelectionFromObjectIds } = useSelectionUtilities()
|
||||
|
||||
const {
|
||||
viewer: {
|
||||
metadata: { filteringState }
|
||||
@@ -306,10 +309,11 @@ const unhighlightObject = () => {
|
||||
if (refObject && typeof refObject === 'string') unhighlightObjects([refObject])
|
||||
}
|
||||
|
||||
const selectObject = () => {
|
||||
// Only expand if not already expanded
|
||||
const handleClick = () => {
|
||||
if (!props.isExpanded) {
|
||||
emit('toggle-expansion')
|
||||
} else {
|
||||
addToSelectionFromObjectIds(modelObjectIds.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<button
|
||||
class="w-full border-b border-outline-3 p-3 cursor-pointer group text-left"
|
||||
:class="getObjectBackgroundClass()"
|
||||
@click="handleObjectClick"
|
||||
@keydown.enter="handleObjectClick"
|
||||
@click="handleClick"
|
||||
@keydown.enter="handleClick"
|
||||
@mouseenter="handleObjectMouseEnter"
|
||||
@mouseleave="handleObjectMouseLeave"
|
||||
>
|
||||
@@ -33,19 +33,14 @@ const props = defineProps<{
|
||||
|
||||
const {
|
||||
objects: selectedObjects,
|
||||
addToSelection,
|
||||
addToSelectionFromObjectIds,
|
||||
clearSelection
|
||||
} = useSelectionUtilities()
|
||||
const { highlightObjects, unhighlightObjects } = useHighlightedObjectsUtilities()
|
||||
|
||||
const handleObjectClick = () => {
|
||||
const objectData = {
|
||||
id: props.objectId,
|
||||
speckle_type: 'Base' // eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
clearSelection()
|
||||
addToSelection(objectData)
|
||||
addToSelectionFromObjectIds([props.objectId])
|
||||
}
|
||||
|
||||
const handleObjectMouseEnter = () => {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events -->
|
||||
<template>
|
||||
<button
|
||||
class="w-full h-16 pr-2 py-2 cursor-pointer group text-left bg-foundation hover:bg-highlight-1 border-b border-outline-3"
|
||||
:class="getObjectBackgroundClass()"
|
||||
@click="handleObjectClick"
|
||||
@keydown.enter="handleObjectClick"
|
||||
@mouseenter="handleObjectMouseEnter"
|
||||
@mouseleave="handleObjectMouseLeave"
|
||||
>
|
||||
<div class="flex items-center gap-1 h-full">
|
||||
<ViewerExpansionTriangle
|
||||
class="h-8"
|
||||
:is-expanded="isExpanded"
|
||||
@click="$emit('toggleExpansion', objectId)"
|
||||
/>
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<CubeIcon class="w-4 h-4 shrink-0" />
|
||||
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<div class="text-body-2xs font-medium text-foreground truncate">
|
||||
Detached Object
|
||||
</div>
|
||||
<div class="text-body-3xs text-foreground-2 truncate">
|
||||
{{ objectId }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CubeIcon } from '@heroicons/vue/24/outline'
|
||||
import {
|
||||
useSelectionUtilities,
|
||||
useHighlightedObjectsUtilities
|
||||
} from '~~/lib/viewer/composables/ui'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import type { ExplorerNode } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
|
||||
const props = defineProps<{
|
||||
objectId: string
|
||||
isExpanded?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleExpansion: [objectId: string]
|
||||
}>()
|
||||
|
||||
const {
|
||||
viewer: {
|
||||
metadata: { worldTree }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const {
|
||||
objects: selectedObjects,
|
||||
addToSelectionFromObjectIds,
|
||||
clearSelection
|
||||
} = useSelectionUtilities()
|
||||
const { highlightObjects, unhighlightObjects } = useHighlightedObjectsUtilities()
|
||||
|
||||
const objectNode = computed(() => {
|
||||
if (!worldTree.value) return null
|
||||
|
||||
const rootNodes = worldTree.value._root.children
|
||||
return rootNodes?.find((node: ExplorerNode) => {
|
||||
const nodeObjectId = ((node.model as Record<string, unknown>).id as string)
|
||||
.split('/')
|
||||
.reverse()[0] as string
|
||||
return nodeObjectId === props.objectId
|
||||
})
|
||||
})
|
||||
|
||||
const hasHierarchy = computed(() => {
|
||||
return objectNode.value?.model?.children && objectNode.value.model.children.length > 0
|
||||
})
|
||||
|
||||
const handleObjectClick = () => {
|
||||
if (hasHierarchy.value) {
|
||||
if (!props.isExpanded) {
|
||||
emit('toggleExpansion', props.objectId)
|
||||
} else {
|
||||
clearSelection()
|
||||
addToSelectionFromObjectIds([props.objectId])
|
||||
}
|
||||
} else {
|
||||
clearSelection()
|
||||
addToSelectionFromObjectIds([props.objectId])
|
||||
}
|
||||
}
|
||||
|
||||
const handleObjectMouseEnter = () => {
|
||||
if (props.objectId && typeof props.objectId === 'string') {
|
||||
highlightObjects([props.objectId])
|
||||
}
|
||||
}
|
||||
|
||||
const handleObjectMouseLeave = () => {
|
||||
if (props.objectId && typeof props.objectId === 'string') {
|
||||
unhighlightObjects([props.objectId])
|
||||
}
|
||||
}
|
||||
|
||||
const getObjectBackgroundClass = (): string => {
|
||||
const isSelected = selectedObjects.value.find((o) => o.id === props.objectId)
|
||||
|
||||
if (isSelected) return 'bg-highlight-3'
|
||||
return 'bg-foundation hover:bg-highlight-1'
|
||||
}
|
||||
</script>
|
||||
@@ -27,18 +27,10 @@
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<template v-if="resourceItems.length || objects.length">
|
||||
<!-- Detached Objects Section -->
|
||||
<div v-if="objects.length > 0">
|
||||
<ViewerModelsDetachedObjectCard
|
||||
v-for="object in objects"
|
||||
:key="object.objectId"
|
||||
:object-id="object.objectId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Sticky Header Area (outside virtual list) -->
|
||||
<div v-if="stickyHeader" class="sticky top-0 z-20 h-16">
|
||||
<ViewerModelsCard
|
||||
v-if="!isDetachedObjectSticky"
|
||||
:model="stickyHeader!.model"
|
||||
:version-id="stickyHeader!.versionId"
|
||||
:is-expanded="expandedModels.has(stickyHeader!.model.id)"
|
||||
@@ -46,6 +38,12 @@
|
||||
@show-versions="handleShowVersions"
|
||||
@show-diff="handleShowDiff"
|
||||
/>
|
||||
<ViewerModelsDetachedObjectHeader
|
||||
v-else
|
||||
:object-id="stickyHeader!.versionId"
|
||||
:is-expanded="expandedModels.has(stickyHeader!.model.id)"
|
||||
@toggle-expansion="toggleModelExpansion"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -54,16 +52,17 @@
|
||||
v-bind="containerProps"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-bind="wrapperProps">
|
||||
<div v-bind="wrapperProps" class="model-list">
|
||||
<div
|
||||
v-for="{ data: item } in virtualList"
|
||||
:key="item.id"
|
||||
:data-item-id="item.id"
|
||||
:data-item-type="item.type"
|
||||
class="group first:hidden"
|
||||
>
|
||||
<!-- Model Header -->
|
||||
<template v-if="item.type === 'model-header'">
|
||||
<div class="bg-foundation h-16">
|
||||
<div class="bg-foundation h-16 model-header">
|
||||
<ViewerModelsCard
|
||||
:model="getModelFromItem(item)"
|
||||
:version-id="getVersionIdFromItem(item)"
|
||||
@@ -75,6 +74,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Detached Object Header -->
|
||||
<template v-else-if="item.type === 'detached-object-header'">
|
||||
<div class="bg-foundation h-16 model-header">
|
||||
<ViewerModelsDetachedObjectHeader
|
||||
:object-id="getObjectIdFromItem(item)"
|
||||
:is-expanded="expandedModels.has(item.modelId)"
|
||||
@toggle-expansion="toggleModelExpansion(item.modelId)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Tree Item -->
|
||||
<template v-else-if="item.type === 'tree-item'">
|
||||
<ViewerModelsVirtualTreeItem
|
||||
@@ -178,6 +188,7 @@ const unifiedVirtualItems = computed(() => {
|
||||
selectedObjects.value,
|
||||
worldTree.value || null,
|
||||
stateResourceItems.value as { objectId: string; modelId?: string }[],
|
||||
objects.value,
|
||||
getRootNodesForModel,
|
||||
flattenModelTree
|
||||
)
|
||||
@@ -190,7 +201,9 @@ const {
|
||||
} = useVirtualList(unifiedVirtualItems, {
|
||||
itemHeight: (index) => {
|
||||
const item = unifiedVirtualItems.value[index]
|
||||
return item?.type === 'model-header' ? 64 : 40
|
||||
return item?.type === 'model-header' || item?.type === 'detached-object-header'
|
||||
? 64
|
||||
: 40
|
||||
},
|
||||
overscan: 20
|
||||
})
|
||||
@@ -207,7 +220,8 @@ const modelHeaderPositions = computed(() => {
|
||||
let cumulativeHeight = 0
|
||||
for (let i = 0; i < unifiedVirtualItems.value.length; i++) {
|
||||
const item = unifiedVirtualItems.value[i]
|
||||
const itemHeight = item.type === 'model-header' ? 64 : 40
|
||||
const itemHeight =
|
||||
item.type === 'model-header' || item.type === 'detached-object-header' ? 64 : 40
|
||||
|
||||
if (item.type === 'model-header') {
|
||||
const data = item.data as { model: ModelItem; versionId: string }
|
||||
@@ -217,6 +231,20 @@ const modelHeaderPositions = computed(() => {
|
||||
versionId: data.versionId,
|
||||
position: cumulativeHeight
|
||||
})
|
||||
} else if (item.type === 'detached-object-header') {
|
||||
const data = item.data as { objectId: string }
|
||||
// Create a detached object header item in the virtual list
|
||||
const detachedObjectHeader = {
|
||||
id: data.objectId,
|
||||
name: 'Detached Object',
|
||||
displayName: 'Detached Object'
|
||||
} as unknown as ModelItem
|
||||
headers.push({
|
||||
index: i,
|
||||
model: detachedObjectHeader,
|
||||
versionId: data.objectId,
|
||||
position: cumulativeHeight
|
||||
})
|
||||
}
|
||||
cumulativeHeight += itemHeight
|
||||
}
|
||||
@@ -227,6 +255,11 @@ const hasDiffActive = computed(() => {
|
||||
return !!(diffState.oldVersion.value && diffState.newVersion.value)
|
||||
})
|
||||
|
||||
const isDetachedObjectSticky = computed(() => {
|
||||
if (!stickyHeader.value) return false
|
||||
return objects.value.some((obj) => obj.objectId === stickyHeader.value?.model.id)
|
||||
})
|
||||
|
||||
const handleShowVersions = (modelId: string) => {
|
||||
expandedModelId.value = modelId
|
||||
subView.value = ModelsSubView.Versions
|
||||
@@ -314,6 +347,13 @@ const getVersionIdFromItem = (item: UnifiedVirtualItem): string => {
|
||||
return ''
|
||||
}
|
||||
|
||||
const getObjectIdFromItem = (item: UnifiedVirtualItem): string => {
|
||||
if (item.type === 'detached-object-header') {
|
||||
return (item.data as { objectId: string }).objectId
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const scrollToSelectedItem = (objectId: string) => {
|
||||
nextTick(() => {
|
||||
const itemIndex = unifiedVirtualItems.value.findIndex(
|
||||
@@ -425,14 +465,27 @@ watch(
|
||||
unifiedVirtualItems,
|
||||
(items) => {
|
||||
if (items.length > 0) {
|
||||
const firstModelHeader = items.find((item) => item.type === 'model-header')
|
||||
if (firstModelHeader) {
|
||||
const data = firstModelHeader.data as { model: ModelItem; versionId: string }
|
||||
|
||||
// Always update to the current first model (handles new models being added)
|
||||
stickyHeader.value = {
|
||||
model: data.model,
|
||||
versionId: data.versionId
|
||||
const firstHeader = items.find(
|
||||
(item) => item.type === 'model-header' || item.type === 'detached-object-header'
|
||||
)
|
||||
if (firstHeader) {
|
||||
if (firstHeader.type === 'model-header') {
|
||||
const data = firstHeader.data as { model: ModelItem; versionId: string }
|
||||
stickyHeader.value = {
|
||||
model: data.model,
|
||||
versionId: data.versionId
|
||||
}
|
||||
} else if (firstHeader.type === 'detached-object-header') {
|
||||
const data = firstHeader.data as { objectId: string }
|
||||
const detachedObjectHeader = {
|
||||
id: data.objectId,
|
||||
name: 'Detached Object',
|
||||
displayName: 'Detached Object'
|
||||
} as unknown as ModelItem
|
||||
stickyHeader.value = {
|
||||
model: detachedObjectHeader,
|
||||
versionId: data.objectId
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -442,3 +495,17 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add border-top to model/detached object headers that follow tree items using css */
|
||||
.model-list
|
||||
.group[data-item-type='tree-item']
|
||||
+ .group[data-item-type='model-header']
|
||||
.model-header,
|
||||
.model-list
|
||||
.group[data-item-type='tree-item']
|
||||
+ .group[data-item-type='detached-object-header']
|
||||
.model-header {
|
||||
@apply border-t border-outline-3;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ViewerCommentsPortalOrDiv class="relative" to="bottomPanel">
|
||||
<ViewerControlsRight
|
||||
v-if="isGreaterThanSm"
|
||||
v-if="isGreaterThanSm && showControls"
|
||||
:sidebar-open="sidebarOpen && shouldRenderSidebar"
|
||||
:sidebar-width="sidebarWidth"
|
||||
/>
|
||||
@@ -90,6 +90,7 @@ import { modelRoute } from '~/lib/common/helpers/route'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
import { Ellipsis } from 'lucide-vue-next'
|
||||
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
||||
|
||||
enum ActionTypes {
|
||||
OpenInNewTab = 'open-in-new-tab'
|
||||
@@ -112,6 +113,7 @@ const breakpoints = useBreakpoints(TailwindBreakpoints)
|
||||
const isGreaterThanSm = breakpoints.greater('sm')
|
||||
const menuId = useId()
|
||||
const mp = useMixpanel()
|
||||
const { showControls } = useEmbed()
|
||||
|
||||
const itemCount = ref(20)
|
||||
const sidebarOpen = ref(false)
|
||||
|
||||
@@ -46,11 +46,10 @@ type Documents = {
|
||||
"\n fragment DashboardsCard_Dashboard on Dashboard {\n id\n name\n createdAt\n workspace {\n id\n }\n createdBy {\n id\n name\n avatar\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.DashboardsCard_DashboardFragmentDoc,
|
||||
"\n fragment DashboardsEditDialog_Dashboard on Dashboard {\n id\n name\n workspace {\n id\n }\n }\n": typeof types.DashboardsEditDialog_DashboardFragmentDoc,
|
||||
"\n query DashboardsListCanCreateDashboards($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canCreateDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.DashboardsListCanCreateDashboardsDocument,
|
||||
"\n query DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.DashboardsSharDialogPermissionsDocument,
|
||||
"\n query DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n }\n }\n": typeof types.DashboardsSharDialogPermissionsDocument,
|
||||
"\n mutation DashboardsShareToken($dashboardId: String!) {\n dashboardMutations {\n share(dashboardId: $dashboardId) {\n id\n revoked\n content\n }\n }\n }\n": typeof types.DashboardsShareTokenDocument,
|
||||
"\n mutation DashboardsShareEnableToken($input: DashboardShareInput!) {\n dashboardMutations {\n enableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n": typeof types.DashboardsShareEnableTokenDocument,
|
||||
"\n mutation DashboardsShareDisableToken($input: DashboardShareInput!) {\n dashboardMutations {\n disableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n": typeof types.DashboardsShareDisableTokenDocument,
|
||||
"\n query DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.DashboardsSharePermissionsDocument,
|
||||
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": typeof types.FormSelectModels_ModelFragmentDoc,
|
||||
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc,
|
||||
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": typeof types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
|
||||
@@ -70,8 +69,8 @@ type Documents = {
|
||||
"\n query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n id\n invitableCollaborators(filter: $filter) {\n items {\n ...InviteProjectItem_WorkspaceCollaborator\n }\n }\n }\n }\n": typeof types.InviteDialogProjectRowProjectCollaboratorsDocument,
|
||||
"\n fragment InviteDialogSharedSelectUsers_Workspace on Workspace {\n id\n slug\n defaultSeatType\n }\n": typeof types.InviteDialogSharedSelectUsers_WorkspaceFragmentDoc,
|
||||
"\n fragment PresentationHeader_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": typeof types.PresentationHeader_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n }\n": typeof types.PresentationInfoSidebar_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n }\n": typeof types.PresentationLeftSidebar_WorkspaceFragmentDoc,
|
||||
"\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.PresentationInfoSidebar_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n": typeof types.PresentationLeftSidebar_WorkspaceFragmentDoc,
|
||||
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n": typeof types.PresentationSlideEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n": typeof types.PresentationSlideListSlide_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideList_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n items {\n id\n ...PresentationSlideListSlide_SavedView\n }\n }\n }\n": typeof types.PresentationSlideList_SavedViewGroupFragmentDoc,
|
||||
@@ -544,7 +543,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": typeof types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n ...SettingsWorkspacesSecurityDefaultSeat_Workspace\n ...SettingsWorkspacesSecurityDomainManagement_Workspace\n ...SettingsWorkspacesSecurityDiscoverability_Workspace\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n ...SettingsWorkspacesSecurityDomainProtection_Workspace\n ...SettingsWorkspacesSecurityWorkspaceCreation_Workspace\n id\n slug\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n }\n": typeof types.WorkspaceDashboards_DashboardFragmentDoc,
|
||||
"\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n permissions {\n canEdit {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.WorkspaceDashboards_DashboardFragmentDoc,
|
||||
"\n fragment WorkspacePage_Workspace on Workspace {\n ...WorkspaceDashboard_Workspace\n ...WorkspaceSidebar_Workspace\n }\n": typeof types.WorkspacePage_WorkspaceFragmentDoc,
|
||||
};
|
||||
const documents: Documents = {
|
||||
@@ -580,11 +579,10 @@ const documents: Documents = {
|
||||
"\n fragment DashboardsCard_Dashboard on Dashboard {\n id\n name\n createdAt\n workspace {\n id\n }\n createdBy {\n id\n name\n avatar\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.DashboardsCard_DashboardFragmentDoc,
|
||||
"\n fragment DashboardsEditDialog_Dashboard on Dashboard {\n id\n name\n workspace {\n id\n }\n }\n": types.DashboardsEditDialog_DashboardFragmentDoc,
|
||||
"\n query DashboardsListCanCreateDashboards($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canCreateDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.DashboardsListCanCreateDashboardsDocument,
|
||||
"\n query DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.DashboardsSharDialogPermissionsDocument,
|
||||
"\n query DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n }\n }\n": types.DashboardsSharDialogPermissionsDocument,
|
||||
"\n mutation DashboardsShareToken($dashboardId: String!) {\n dashboardMutations {\n share(dashboardId: $dashboardId) {\n id\n revoked\n content\n }\n }\n }\n": types.DashboardsShareTokenDocument,
|
||||
"\n mutation DashboardsShareEnableToken($input: DashboardShareInput!) {\n dashboardMutations {\n enableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n": types.DashboardsShareEnableTokenDocument,
|
||||
"\n mutation DashboardsShareDisableToken($input: DashboardShareInput!) {\n dashboardMutations {\n disableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n": types.DashboardsShareDisableTokenDocument,
|
||||
"\n query DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.DashboardsSharePermissionsDocument,
|
||||
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc,
|
||||
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
|
||||
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
|
||||
@@ -604,8 +602,8 @@ const documents: Documents = {
|
||||
"\n query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n id\n invitableCollaborators(filter: $filter) {\n items {\n ...InviteProjectItem_WorkspaceCollaborator\n }\n }\n }\n }\n": types.InviteDialogProjectRowProjectCollaboratorsDocument,
|
||||
"\n fragment InviteDialogSharedSelectUsers_Workspace on Workspace {\n id\n slug\n defaultSeatType\n }\n": types.InviteDialogSharedSelectUsers_WorkspaceFragmentDoc,
|
||||
"\n fragment PresentationHeader_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": types.PresentationHeader_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n }\n": types.PresentationInfoSidebar_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n }\n": types.PresentationLeftSidebar_WorkspaceFragmentDoc,
|
||||
"\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.PresentationInfoSidebar_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n": types.PresentationLeftSidebar_WorkspaceFragmentDoc,
|
||||
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n": types.PresentationSlideEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n": types.PresentationSlideListSlide_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideList_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n items {\n id\n ...PresentationSlideListSlide_SavedView\n }\n }\n }\n": types.PresentationSlideList_SavedViewGroupFragmentDoc,
|
||||
@@ -1078,7 +1076,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n ...SettingsWorkspacesSecurityDefaultSeat_Workspace\n ...SettingsWorkspacesSecurityDomainManagement_Workspace\n ...SettingsWorkspacesSecurityDiscoverability_Workspace\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n ...SettingsWorkspacesSecurityDomainProtection_Workspace\n ...SettingsWorkspacesSecurityWorkspaceCreation_Workspace\n id\n slug\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n }\n": types.WorkspaceDashboards_DashboardFragmentDoc,
|
||||
"\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n permissions {\n canEdit {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.WorkspaceDashboards_DashboardFragmentDoc,
|
||||
"\n fragment WorkspacePage_Workspace on Workspace {\n ...WorkspaceDashboard_Workspace\n ...WorkspaceSidebar_Workspace\n }\n": types.WorkspacePage_WorkspaceFragmentDoc,
|
||||
};
|
||||
|
||||
@@ -1227,7 +1225,7 @@ export function graphql(source: "\n query DashboardsListCanCreateDashboards($sl
|
||||
/**
|
||||
* 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 DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"): (typeof documents)["\n query DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n }\n }\n"): (typeof documents)["\n query DashboardsSharDialogPermissions($id: String!) {\n dashboard(id: $id) {\n id\n shareLink {\n id\n content\n revoked\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1240,10 +1238,6 @@ export function graphql(source: "\n mutation DashboardsShareEnableToken($input:
|
||||
* 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 DashboardsShareDisableToken($input: DashboardShareInput!) {\n dashboardMutations {\n disableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n"): (typeof documents)["\n mutation DashboardsShareDisableToken($input: DashboardShareInput!) {\n dashboardMutations {\n disableShare(input: $input) {\n id\n revoked\n content\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 DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"): (typeof documents)["\n query DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1323,11 +1317,11 @@ export function graphql(source: "\n fragment PresentationHeader_SavedViewGroup
|
||||
/**
|
||||
* 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 PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n }\n"): (typeof documents)["\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n }\n"];
|
||||
export function graphql(source: "\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\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 PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n }\n"): (typeof documents)["\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n }\n"];
|
||||
export function graphql(source: "\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n"): (typeof documents)["\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -3219,7 +3213,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesSecurity_Workspa
|
||||
/**
|
||||
* 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 WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n }\n"): (typeof documents)["\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n permissions {\n canEdit {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n permissions {\n canEdit {\n ...FullPermissionCheckResult\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
@@ -6,13 +6,20 @@ import {
|
||||
} from '~/lib/common/helpers/graphql'
|
||||
import type { UpdateSavedViewInput } from '~/lib/common/generated/gql/graphql'
|
||||
import { updatePresentationSlideMutation } from '~/lib/presentations/graphql/mutations'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
|
||||
export const useUpdatePresentationSlide = () => {
|
||||
const { mutate, loading } = useMutation(updatePresentationSlideMutation)
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
return {
|
||||
mutate: async (input: UpdateSavedViewInput) => {
|
||||
mutate: async (params: {
|
||||
input: UpdateSavedViewInput
|
||||
workspaceId: MaybeNullOrUndefined<string>
|
||||
}) => {
|
||||
const { input, workspaceId } = params
|
||||
const result = await mutate({ input }).catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (result?.data?.projectMutations.savedViewMutations.updateView) {
|
||||
@@ -20,6 +27,11 @@ export const useUpdatePresentationSlide = () => {
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Slide updated'
|
||||
})
|
||||
|
||||
mixpanel.track('Presentation Slide Updated', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspaceId
|
||||
})
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { FilterData } from '~/lib/viewer/helpers/filters/types'
|
||||
import type { SpeckleObject } from '@speckle/viewer'
|
||||
import type { Raw } from 'vue'
|
||||
import { FilteringExtension } from '@speckle/viewer'
|
||||
import { FilteringExtension, SelectionExtension } from '@speckle/viewer'
|
||||
import { watchTriggerable } from '@vueuse/core'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
import { useOnViewerLoadComplete } from '~/lib/viewer/composables/viewer'
|
||||
import { useFilteringDataStore } from '~/lib/viewer/composables/filtering/dataStore'
|
||||
import { HighlightExtension } from '~/lib/viewer/composables/setup/highlighting'
|
||||
|
||||
/**
|
||||
* Setup composable for filter-related state
|
||||
@@ -61,6 +62,39 @@ export const useManualFilteringPostSetup = () => {
|
||||
|
||||
const filteringExtension = () => instance.getExtension(FilteringExtension)
|
||||
|
||||
/**
|
||||
* Preserve selection and highlighting state during filtering operations
|
||||
* This replicates LegacyViewer's preserveSelectionHighlightFilter function
|
||||
*/
|
||||
const preserveSelectionHighlightFilter = <T>(filterFn: () => T): T => {
|
||||
const selectionExtension = instance.getExtension(SelectionExtension)
|
||||
const highlightExtension = instance.getExtension(HighlightExtension)
|
||||
|
||||
// 1. SAVE current state from viewer extensions
|
||||
const selectedObjects = selectionExtension
|
||||
.getSelectedObjects()
|
||||
.map((obj) => obj.id as string)
|
||||
const highlightedObjects =
|
||||
highlightExtension?.getSelectedObjects().map((obj) => obj.id as string) || []
|
||||
|
||||
// 2. CLEAR viewer extensions directly
|
||||
if (selectedObjects.length) selectionExtension.clearSelection()
|
||||
if (highlightedObjects.length && highlightExtension) {
|
||||
highlightExtension.clearSelection()
|
||||
}
|
||||
|
||||
// 3. EXECUTE the filtering operation
|
||||
const result = filterFn()
|
||||
|
||||
// 4. RESTORE to viewer extensions directly
|
||||
if (selectedObjects.length) selectionExtension.selectObjects(selectedObjects)
|
||||
if (highlightedObjects.length && highlightExtension) {
|
||||
highlightExtension.selectObjects(highlightedObjects)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes to manually isolated object IDs
|
||||
*/
|
||||
@@ -69,17 +103,19 @@ export const useManualFilteringPostSetup = () => {
|
||||
(newIds, oldIds) => {
|
||||
if (!newIds || !oldIds) return
|
||||
|
||||
const extension = filteringExtension()
|
||||
preserveSelectionHighlightFilter(() => {
|
||||
const extension = filteringExtension()
|
||||
|
||||
const toIsolate = newIds.filter((id) => !oldIds.includes(id))
|
||||
if (toIsolate.length > 0) {
|
||||
extension.isolateObjects(toIsolate, 'manual-isolation', true, true)
|
||||
}
|
||||
const toIsolate = newIds.filter((id) => !oldIds.includes(id))
|
||||
if (toIsolate.length > 0) {
|
||||
extension.isolateObjects(toIsolate, 'manual-isolation', true, true)
|
||||
}
|
||||
|
||||
const toUnIsolate = oldIds.filter((id) => !newIds.includes(id))
|
||||
if (toUnIsolate.length > 0) {
|
||||
extension.unIsolateObjects(toUnIsolate, 'manual-isolation', true, true)
|
||||
}
|
||||
const toUnIsolate = oldIds.filter((id) => !newIds.includes(id))
|
||||
if (toUnIsolate.length > 0) {
|
||||
extension.unIsolateObjects(toUnIsolate, 'manual-isolation', true, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -92,17 +128,19 @@ export const useManualFilteringPostSetup = () => {
|
||||
(newIds, oldIds) => {
|
||||
if (!newIds || !oldIds) return
|
||||
|
||||
const extension = filteringExtension()
|
||||
preserveSelectionHighlightFilter(() => {
|
||||
const extension = filteringExtension()
|
||||
|
||||
const toHide = newIds.filter((id) => !oldIds.includes(id))
|
||||
if (toHide.length > 0) {
|
||||
extension.hideObjects(toHide, 'manual-hiding', false, false)
|
||||
}
|
||||
const toHide = newIds.filter((id) => !oldIds.includes(id))
|
||||
if (toHide.length > 0) {
|
||||
extension.hideObjects(toHide, 'manual-hiding', false, false)
|
||||
}
|
||||
|
||||
const toShow = oldIds.filter((id) => !newIds.includes(id))
|
||||
if (toShow.length > 0) {
|
||||
extension.showObjects(toShow, 'manual-hiding', false)
|
||||
}
|
||||
const toShow = oldIds.filter((id) => !newIds.includes(id))
|
||||
if (toShow.length > 0) {
|
||||
extension.showObjects(toShow, 'manual-hiding', false)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
} from '@speckle/viewer'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
import { useOnViewerLoadComplete } from '~/lib/viewer/composables/viewer'
|
||||
import { ViewerRenderPageType } from '~/lib/viewer/helpers/state'
|
||||
|
||||
/**
|
||||
* Highlighting extension that replicates LegacyViewer's HighlightExtension
|
||||
* Uses SelectionExtension but disables default events for UI-only highlighting
|
||||
*/
|
||||
class HighlightExtension extends SelectionExtension {
|
||||
export class HighlightExtension extends SelectionExtension {
|
||||
public constructor(viewer: IViewer, cameraProvider: CameraController) {
|
||||
super(viewer, cameraProvider)
|
||||
|
||||
@@ -44,23 +45,22 @@ class HighlightExtension extends SelectionExtension {
|
||||
export const useHighlightingPostSetup = () => {
|
||||
const {
|
||||
ui: { highlightedObjectIds },
|
||||
viewer: { instance }
|
||||
viewer: { instance },
|
||||
pageType
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const highlightExtension = ref<HighlightExtension | null>(null)
|
||||
if (pageType.value === ViewerRenderPageType.Presentation) return
|
||||
|
||||
// Create the highlighting extension once during setup
|
||||
instance.createExtension(HighlightExtension)
|
||||
|
||||
// Get the highlighting extension instance
|
||||
const getHighlightExtension = () => {
|
||||
if (!highlightExtension.value) {
|
||||
highlightExtension.value = instance.createExtension(HighlightExtension)
|
||||
}
|
||||
return highlightExtension.value
|
||||
}
|
||||
const getHighlightExtensionInstance = () => instance.getExtension(HighlightExtension)
|
||||
|
||||
useOnViewerLoadComplete(
|
||||
({ isInitial }) => {
|
||||
if (!isInitial) return
|
||||
getHighlightExtension()
|
||||
getHighlightExtensionInstance()
|
||||
},
|
||||
{ initialOnly: true }
|
||||
)
|
||||
@@ -69,7 +69,7 @@ export const useHighlightingPostSetup = () => {
|
||||
watch(
|
||||
highlightedObjectIds,
|
||||
(newIds, oldIds) => {
|
||||
const extension = getHighlightExtension()
|
||||
const extension = getHighlightExtensionInstance()
|
||||
if (!extension) return
|
||||
|
||||
// Clear all current highlights if new list is empty
|
||||
@@ -79,8 +79,6 @@ export const useHighlightingPostSetup = () => {
|
||||
}
|
||||
|
||||
if (oldIds && isEqual(newIds, oldIds)) return
|
||||
|
||||
// Clear and re-select to avoid accumulation
|
||||
extension.clearSelection()
|
||||
if (newIds.length > 0) {
|
||||
extension.selectObjects(newIds)
|
||||
|
||||
@@ -67,7 +67,10 @@ import {
|
||||
} from '~/lib/viewer/composables/setup/filters'
|
||||
import { useFilterUtilities } from '~/lib/viewer/composables/filtering/filtering'
|
||||
import { useFilteringSetup } from '~/lib/viewer/composables/filtering/setup'
|
||||
import { useHighlightingPostSetup } from '~/lib/viewer/composables/setup/highlighting'
|
||||
import {
|
||||
useHighlightingPostSetup,
|
||||
HighlightExtension
|
||||
} from '~/lib/viewer/composables/setup/highlighting'
|
||||
|
||||
function useViewerLoadCompleteEventHandler() {
|
||||
const state = useInjectedViewerState()
|
||||
@@ -566,17 +569,33 @@ function useViewerFiltersIntegration() {
|
||||
).filter(isNonNullable)
|
||||
if (arraysEqual(newIds, oldIds)) return
|
||||
|
||||
state.ui.highlightedObjectIds.value = []
|
||||
|
||||
const selectionExtension = instance.getExtension(SelectionExtension)
|
||||
const currentViewerSelection = selectionExtension
|
||||
.getSelectedObjects()
|
||||
.map((obj) => obj.id as string)
|
||||
|
||||
if (
|
||||
currentViewerSelection.length === newIds.length &&
|
||||
difference(currentViewerSelection, newIds).length === 0
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
state.ui.highlightedObjectIds.value = []
|
||||
const highlightExtension = instance.getExtension(HighlightExtension)
|
||||
if (highlightExtension) {
|
||||
highlightExtension.clearSelection()
|
||||
}
|
||||
|
||||
selectionExtension.clearSelection()
|
||||
|
||||
if (newVal.length > 0) {
|
||||
selectionExtension.selectObjects(newIds)
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: 'sync' }
|
||||
{
|
||||
immediate: true,
|
||||
flush: 'sync'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { useCameraUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useSelectionEvents } from '~~/lib/viewer/composables/viewer'
|
||||
import { ViewerRenderPageType } from '~/lib/viewer/helpers/state'
|
||||
|
||||
function useCollectSelection() {
|
||||
const {
|
||||
@@ -46,8 +47,11 @@ function useSelectOrZoomOnSelection() {
|
||||
if (args.hits.length === 0) return trackAndClearSelection()
|
||||
if (!args.multiple) clearSelection() // note we're not tracking selectino clearing here
|
||||
|
||||
// Skip if selection disabled
|
||||
if (!state.viewer.instance.getExtension(SelectionExtension).enabled) {
|
||||
// Skip if selection disabled or in presentation mode
|
||||
if (
|
||||
!state.viewer.instance.getExtension(SelectionExtension).enabled ||
|
||||
state.pageType.value === ViewerRenderPageType.Presentation
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@ const isReferencedIdArray = (value: unknown): value is { referencedId: string }[
|
||||
}
|
||||
|
||||
export type UnifiedVirtualItem = {
|
||||
type: 'model-header' | 'tree-item'
|
||||
type: 'model-header' | 'tree-item' | 'detached-object-header'
|
||||
id: string
|
||||
modelId: string
|
||||
data: ExplorerNode | ModelWithVersion
|
||||
data: ExplorerNode | ModelWithVersion | { objectId: string }
|
||||
indent?: number
|
||||
hasChildren?: boolean
|
||||
isExpanded?: boolean
|
||||
@@ -51,7 +51,7 @@ export type UnifiedVirtualItem = {
|
||||
}
|
||||
|
||||
function createTreeStateManager() {
|
||||
const flattenedTreeCache = reactive(new Map<string, UnifiedVirtualItem[]>())
|
||||
const flattenedTreeCache = new Map<string, UnifiedVirtualItem[]>()
|
||||
const lastCacheKey = ref('')
|
||||
const isInitialized = ref(false)
|
||||
|
||||
@@ -75,7 +75,8 @@ function createTreeStateManager() {
|
||||
modelsAndVersionIds: { model: ModelItem; versionId: string }[],
|
||||
expandedModels: Set<string>,
|
||||
expandedNodes: Set<string>,
|
||||
selectedObjects: { id: string }[]
|
||||
selectedObjects: { id: string }[],
|
||||
detachedObjects: { objectId: string }[]
|
||||
): string => {
|
||||
const parts = [
|
||||
modelsAndVersionIds
|
||||
@@ -86,6 +87,10 @@ function createTreeStateManager() {
|
||||
selectedObjects
|
||||
.map((o) => o.id)
|
||||
.sort()
|
||||
.join(','),
|
||||
detachedObjects
|
||||
.map((o) => o.objectId)
|
||||
.sort()
|
||||
.join(',')
|
||||
]
|
||||
return parts.join('#')
|
||||
@@ -115,6 +120,7 @@ function createTreeStateManager() {
|
||||
selectedObjects: { id: string }[],
|
||||
worldTree: WorldTree | null,
|
||||
stateResourceItems: { objectId: string; modelId?: string }[],
|
||||
detachedObjects: { objectId: string }[],
|
||||
getRootNodesForModel: (
|
||||
modelId: string,
|
||||
worldTree: WorldTree | null,
|
||||
@@ -134,7 +140,8 @@ function createTreeStateManager() {
|
||||
modelsAndVersionIds,
|
||||
expandedModels,
|
||||
expandedNodes,
|
||||
selectedObjects
|
||||
selectedObjects,
|
||||
detachedObjects
|
||||
)
|
||||
|
||||
if (lastCacheKey.value === cacheKey && flattenedTreeCache.has(cacheKey)) {
|
||||
@@ -177,6 +184,44 @@ function createTreeStateManager() {
|
||||
}
|
||||
})
|
||||
|
||||
// Handle detached objects
|
||||
detachedObjects.forEach((detachedObject, index) => {
|
||||
const objectId = detachedObject.objectId
|
||||
const isFirstDetachedObject = index === 0 && modelsAndVersionIds.length === 0
|
||||
|
||||
result.push({
|
||||
type: 'detached-object-header',
|
||||
id: `detached-${objectId}`,
|
||||
modelId: objectId, // Use objectId as modelId for detached objects
|
||||
data: { objectId },
|
||||
isFirstModel: isFirstDetachedObject
|
||||
})
|
||||
|
||||
if (expandedModels.has(objectId)) {
|
||||
const detachedRootNodes = getRootNodesForModel(
|
||||
objectId,
|
||||
worldTree,
|
||||
stateResourceItems,
|
||||
modelsAndVersionIds
|
||||
)
|
||||
|
||||
if (detachedRootNodes.length > 0) {
|
||||
const treeItems = flattenModelTree(
|
||||
detachedRootNodes,
|
||||
objectId,
|
||||
expandedNodes,
|
||||
selectedObjects
|
||||
)
|
||||
|
||||
if (treeItems.length > 0) {
|
||||
treeItems[0].isFirstChildOfModel = true
|
||||
treeItems[treeItems.length - 1].isLastChildOfModel = true
|
||||
result.push(...treeItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Cache the result
|
||||
flattenedTreeCache.set(cacheKey, result)
|
||||
lastCacheKey.value = cacheKey
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useInjectedViewerInterfaceState,
|
||||
useInjectedViewerState
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import { ViewerRenderPageType } from '~/lib/viewer/helpers/state'
|
||||
import { useDiffBuilderUtilities } from '~~/lib/viewer/composables/setup/diff'
|
||||
import { getKeyboardShortcutTitle, onKeyboardShortcut } from '@speckle/ui-components'
|
||||
import { ViewerShortcuts } from '~/lib/viewer/helpers/shortcuts/shortcuts'
|
||||
@@ -355,10 +356,13 @@ export function useConditionalViewerRendering() {
|
||||
|
||||
export function useHighlightedObjectsUtilities() {
|
||||
const {
|
||||
ui: { highlightedObjectIds }
|
||||
ui: { highlightedObjectIds },
|
||||
pageType
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const highlightObjects = (ids: string[]) => {
|
||||
if (pageType.value === ViewerRenderPageType.Presentation) return
|
||||
|
||||
highlightedObjectIds.value = [...new Set([...highlightedObjectIds.value, ...ids])]
|
||||
}
|
||||
|
||||
|
||||
@@ -476,6 +476,7 @@ export const useWorkspaceUpdateRole = () => {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('Workspace', input.workspaceId),
|
||||
|
||||
@@ -230,20 +230,6 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
// Redirect old settings - End
|
||||
'/settings/**': {
|
||||
appMiddleware: ['auth', 'settings']
|
||||
},
|
||||
'/settings/server/*': {
|
||||
appMiddleware: ['auth', 'settings', 'admin']
|
||||
},
|
||||
'/settings/workspaces/:slug/*': {
|
||||
appMiddleware: [
|
||||
'auth',
|
||||
'settings',
|
||||
'requires-workspaces-enabled',
|
||||
'require-valid-workspace'
|
||||
]
|
||||
},
|
||||
'/downloads': {
|
||||
redirect: {
|
||||
to: 'https://www.speckle.systems/connectors',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth', 'settings', 'admin'],
|
||||
layout: 'settings'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth', 'settings'],
|
||||
layout: 'settings'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: [
|
||||
'auth',
|
||||
'settings',
|
||||
'requires-workspaces-enabled',
|
||||
'require-valid-workspace'
|
||||
],
|
||||
layout: 'settings'
|
||||
})
|
||||
</script>
|
||||
@@ -3,6 +3,7 @@
|
||||
<Portal to="navigation">
|
||||
<div class="flex items-center">
|
||||
<HeaderNavLink
|
||||
v-if="isLoggedIn"
|
||||
:to="dashboardsRoute(workspace?.slug)"
|
||||
name="Dashboard"
|
||||
:separator="false"
|
||||
@@ -10,8 +11,10 @@
|
||||
<HeaderNavLink
|
||||
:to="dashboardRoute(workspace?.slug, id as string)"
|
||||
:name="dashboard?.name"
|
||||
:separator="isLoggedIn ? true : false"
|
||||
/>
|
||||
<FormButton
|
||||
v-if="canEdit && !hasDashboardToken"
|
||||
v-tippy="'Edit name'"
|
||||
size="sm"
|
||||
color="subtle"
|
||||
@@ -24,7 +27,11 @@
|
||||
</Portal>
|
||||
<Portal to="primary-actions">
|
||||
<div class="flex items-center gap-2">
|
||||
<DashboardsShare :id="dashboard?.id" :workspace-slug="workspace?.slug" />
|
||||
<DashboardsShare
|
||||
v-if="canEdit && !hasDashboardToken"
|
||||
:id="dashboard?.id"
|
||||
:workspace-slug="workspace?.slug"
|
||||
/>
|
||||
<FormButton
|
||||
v-tippy="'Toggle fullscreen'"
|
||||
size="sm"
|
||||
@@ -77,6 +84,11 @@ graphql(`
|
||||
slug
|
||||
logo
|
||||
}
|
||||
permissions {
|
||||
canEdit {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -93,14 +105,19 @@ const { isDarkTheme } = useTheme()
|
||||
const {
|
||||
public: { dashboardsOrigin }
|
||||
} = useRuntimeConfig()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
|
||||
const editDialogOpen = ref(false)
|
||||
|
||||
const hasDashboardToken = computed(() => !!dashboardToken.value)
|
||||
const canEdit = computed(
|
||||
() => result.value?.dashboard?.permissions?.canEdit?.authorized
|
||||
)
|
||||
const workspace = computed(() => result.value?.dashboard?.workspace)
|
||||
const dashboard = computed(() => result.value?.dashboard)
|
||||
const dashboardUrl = computed(
|
||||
() =>
|
||||
`${dashboardsOrigin}/dashboards/${id}?token=${
|
||||
`${dashboardsOrigin}/${dashboardToken.value ? 'view' : 'dashboards'}/${id}?token=${
|
||||
dashboardToken.value || effectiveAuthToken.value
|
||||
}&isEmbed=true&theme=${isDarkTheme.value ? 'dark' : 'light'}`
|
||||
)
|
||||
|
||||
@@ -54,13 +54,14 @@ export const useIntercom = () => {
|
||||
|
||||
// Hide default launcher on viewer routes (/models/)
|
||||
const isViewerRoute = route.path.includes('/models/')
|
||||
const isPresentationRoute = route.path.includes('/presentations/')
|
||||
|
||||
Intercom({
|
||||
/* eslint-disable camelcase */
|
||||
app_id: intercomAppId,
|
||||
user_id: user.value.id || '',
|
||||
created_at: Math.floor(new Date(user.value.createdAt || '').getTime() / 1000),
|
||||
hide_default_launcher: isViewerRoute,
|
||||
hide_default_launcher: isViewerRoute || isPresentationRoute,
|
||||
/* eslint-enable camelcase */
|
||||
name: user.value.name || '',
|
||||
email: user.value.email || ''
|
||||
@@ -104,9 +105,11 @@ export const useIntercom = () => {
|
||||
if (!isInitialized.value) return
|
||||
|
||||
const isViewerRoute = route.path.includes('/models/')
|
||||
const isPresentationRoute = route.path.includes('/presentations/')
|
||||
|
||||
update({
|
||||
/* eslint-disable camelcase */
|
||||
hide_default_launcher: isViewerRoute
|
||||
hide_default_launcher: isViewerRoute || isPresentationRoute
|
||||
/* eslint-enable camelcase */
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('BatchingQueue disposal', () => {
|
||||
|
||||
await queue.disposeAsync()
|
||||
|
||||
expect(processFunction).toHaveBeenCalledWith(items)
|
||||
expect(processFunction).not.toHaveBeenCalled()
|
||||
expect(queue.count()).toBe(0)
|
||||
expect(queue.isDisposed()).toBe(true)
|
||||
})
|
||||
@@ -52,8 +52,6 @@ describe('BatchingQueue disposal', () => {
|
||||
resolveProcess()
|
||||
await disposePromise
|
||||
|
||||
expect(processFunction).toHaveBeenCalledTimes(2)
|
||||
expect(processFunction).toHaveBeenCalledWith(items2)
|
||||
expect(queue.count()).toBe(0)
|
||||
expect(queue.isDisposed()).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CustomLogger } from '../types/functions.js'
|
||||
import KeyedQueue from './keyedQueue.js'
|
||||
|
||||
/**
|
||||
@@ -13,7 +12,6 @@ export default class BatchingQueue<T> {
|
||||
#processFunction: (batch: T[]) => Promise<void>
|
||||
#timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
#isProcessing = false
|
||||
#logger: CustomLogger
|
||||
|
||||
#disposed = false
|
||||
#batchTimeout: number
|
||||
@@ -41,12 +39,10 @@ export default class BatchingQueue<T> {
|
||||
batchSize: number
|
||||
maxWaitTime: number
|
||||
processFunction: (batch: T[]) => Promise<void>
|
||||
logger?: CustomLogger
|
||||
}) {
|
||||
this.#batchSize = params.batchSize
|
||||
this.#processFunction = params.processFunction
|
||||
this.#batchTimeout = params.maxWaitTime
|
||||
this.#logger = params.logger || ((): void => {})
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
@@ -106,10 +102,10 @@ export default class BatchingQueue<T> {
|
||||
if (this.#isProcessing || this.#queue.size === 0) {
|
||||
return
|
||||
}
|
||||
if (this.#disposed) return
|
||||
this.#isProcessing = true
|
||||
|
||||
const batchToProcess = this.#getBatch(this.#batchSize)
|
||||
if (this.#disposed) return
|
||||
|
||||
try {
|
||||
await this.#processFunction(batchToProcess)
|
||||
|
||||
@@ -635,6 +635,9 @@ const getStream = () => {
|
||||
|
||||
// Half a million circles + others stuff
|
||||
// 'https://app.speckle.systems/projects/18d51359fe/models/9f4c5f5947'
|
||||
|
||||
// Revit v3 instances
|
||||
// 'https://app.speckle.systems/projects/03074a2834/models/a013d06fe1@cc11e1ead1'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -706,7 +706,9 @@ export default class SpeckleConverter {
|
||||
instanced: true
|
||||
})
|
||||
this.addNode(instancedNode, transformNode)
|
||||
await this.convertToNode(speckleData, instancedNode)
|
||||
/** Alex 16.09.2025: We're adding support for instance proxies that are not direct displayable types */
|
||||
// await this.convertToNode(speckleData, instancedNode)
|
||||
await this.displayableLookup(speckleData, instancedNode, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user