339 lines
10 KiB
Vue
339 lines
10 KiB
Vue
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
|
<template>
|
|
<aside
|
|
class="absolute left-2 lg:left-0 z-40 flex rounded-lg border border-outline-2 bg-foundation px-1 overflow-visible lg:h-full"
|
|
: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',
|
|
hasActivePanel && 'h-full max-h-[calc(100dvh-8rem)] rounded-r-none'
|
|
]"
|
|
>
|
|
<div class="flex flex-col gap-2 py-1" :class="isEmbedEnabled ? '' : 'lg:py-2'">
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="
|
|
getTooltipProps(
|
|
getShortcutDisplayText(shortcuts.ToggleModels, { format: 'separate' }),
|
|
{
|
|
placement: 'right'
|
|
}
|
|
)
|
|
"
|
|
:active="activePanel === 'models'"
|
|
:icon="'IconViewerModels'"
|
|
@click="toggleActivePanel('models')"
|
|
/>
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="
|
|
getTooltipProps(
|
|
getShortcutDisplayText(shortcuts.ToggleFilters, { format: 'separate' }),
|
|
{
|
|
placement: 'right'
|
|
}
|
|
)
|
|
"
|
|
:active="activePanel === 'filters'"
|
|
:icon="'IconViewerExplorer'"
|
|
:dot="hasActiveFilters"
|
|
@click="toggleActivePanel('filters')"
|
|
/>
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="
|
|
getTooltipProps(
|
|
getShortcutDisplayText(shortcuts.ToggleDiscussions, { format: 'separate' }),
|
|
{
|
|
placement: 'right'
|
|
}
|
|
)
|
|
"
|
|
:active="activePanel === 'discussions'"
|
|
:icon="'IconViewerDiscussions'"
|
|
@click="toggleActivePanel('discussions')"
|
|
/>
|
|
|
|
<!-- Saved views -->
|
|
<ViewerControlsButtonToggle
|
|
v-if="isSavedViewsEnabled"
|
|
v-tippy="getShortcutDisplayText(shortcuts.ToggleSavedViews)"
|
|
:active="activePanel === 'savedViews'"
|
|
:icon="Camera"
|
|
@click="toggleActivePanel('savedViews')"
|
|
></ViewerControlsButtonToggle>
|
|
|
|
<ViewerControlsButtonToggle
|
|
v-if="allAutomationRuns.length !== 0"
|
|
v-tippy="{
|
|
content: summary.longSummary,
|
|
placement: 'right'
|
|
}"
|
|
:active="activePanel === 'automate'"
|
|
@click="toggleActivePanel('automate')"
|
|
>
|
|
<AutomateRunsTriggerStatusIcon
|
|
:summary="summary"
|
|
class="h-5 w-5 md:h-6 md:w-6"
|
|
/>
|
|
</ViewerControlsButtonToggle>
|
|
<div
|
|
v-if="!isEmbedEnabled && (!isTablet || activePanel !== 'none')"
|
|
class="mt-auto flex flex-col gap-2"
|
|
>
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="
|
|
getTooltipProps(
|
|
getShortcutDisplayText(shortcuts.ToggleDevMode, { format: 'separate' }),
|
|
{
|
|
placement: 'right'
|
|
}
|
|
)
|
|
"
|
|
:active="activePanel === 'devMode'"
|
|
:icon="'IconViewerDev'"
|
|
secondary
|
|
@click="toggleActivePanel('devMode')"
|
|
/>
|
|
<ViewerControlsButtonToggle
|
|
v-tippy="
|
|
getTooltipProps('Documentation', {
|
|
placement: 'right'
|
|
})
|
|
"
|
|
:icon="'IconDocs'"
|
|
secondary
|
|
@click="openDocs"
|
|
/>
|
|
<ViewerControlsButtonToggle
|
|
v-if="isIntercomEnabled"
|
|
v-tippy="getTooltipProps('Get help')"
|
|
:icon="'IconIntercom'"
|
|
secondary
|
|
@click="openIntercomChat"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resize handle -->
|
|
<div
|
|
v-if="activePanel !== 'none'"
|
|
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;`"
|
|
@mousedown="startResizing"
|
|
/>
|
|
|
|
<!-- Scrollable controls container -->
|
|
<div
|
|
v-show="activePanel !== 'none'"
|
|
ref="scrollableControlsContainer"
|
|
:class="[
|
|
'bg-foundation absolute z-10 left-[calc(2.5rem+1px)] top-[-1px] bottom-[-1px] overflow-hidden border-outline-2 border border-l-0 rounded-lg rounded-tl-none rounded-bl-none ',
|
|
hasActivePanel ? 'opacity-100' : 'opacity-0',
|
|
isEmbedEnabled ? '' : 'lg:left-[calc(3rem+1px)] lg:border-none lg:rounded-none'
|
|
]"
|
|
:style="`width: ${widthClass};`"
|
|
>
|
|
<KeepAlive v-show="activePanel === 'models'">
|
|
<ViewerModelsPanel v-model:sub-view="modelsSubView" />
|
|
</KeepAlive>
|
|
<KeepAlive v-show="resourceItems.length !== 0 && activePanel === 'filters'">
|
|
<ViewerFiltersPanel />
|
|
</KeepAlive>
|
|
<ViewerCommentsPanel
|
|
v-if="resourceItems.length !== 0 && activePanel === 'discussions'"
|
|
/>
|
|
<AutomateViewerPanel
|
|
v-if="activePanel === 'automate'"
|
|
:automation-runs="allAutomationRuns"
|
|
:summary="summary"
|
|
/>
|
|
<ViewerDataviewerPanel v-if="activePanel === 'devMode'" />
|
|
<ViewerSavedViewsPanel
|
|
v-if="isSavedViewsEnabled && activePanel === 'savedViews'"
|
|
@close="activePanel = 'none'"
|
|
/>
|
|
</div>
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useViewerShortcuts, useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
|
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
|
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
|
import { useEventListener, useResizeObserver, useBreakpoints } from '@vueuse/core'
|
|
import { type Nullable, isNonNullable } from '@speckle/shared'
|
|
import { useInjectedViewerLoadedResources } from '~~/lib/viewer/composables/setup'
|
|
import { useFunctionRunsStatusSummary } from '~/lib/automate/composables/runStatus'
|
|
import { useIntercomEnabled } from '~~/lib/intercom/composables/enabled'
|
|
import { viewerDocsRoute } from '~~/lib/common/helpers/route'
|
|
import { useAreSavedViewsEnabled } from '~/lib/viewer/composables/savedViews/general'
|
|
import { Camera } from 'lucide-vue-next'
|
|
import { ModelsSubView } from '~~/lib/viewer/helpers/sceneExplorer'
|
|
|
|
type ActivePanel =
|
|
| 'none'
|
|
| 'models'
|
|
| 'discussions'
|
|
| 'explorer'
|
|
| 'automate'
|
|
| 'filters'
|
|
| 'devMode'
|
|
| 'savedViews'
|
|
|
|
const emit = defineEmits<{
|
|
forceClosePanels: []
|
|
}>()
|
|
|
|
const width = ref(264)
|
|
const scrollableControlsContainer = ref(null as Nullable<HTMLDivElement>)
|
|
const height = ref(scrollableControlsContainer.value?.clientHeight)
|
|
const isResizing = ref(false)
|
|
const resizeHandle = ref(null)
|
|
let startWidth = 0
|
|
let startX = 0
|
|
|
|
const startResizing = (event: MouseEvent) => {
|
|
if (isMobile.value) return
|
|
event.preventDefault()
|
|
isResizing.value = true
|
|
startX = event.clientX
|
|
startWidth = width.value
|
|
}
|
|
|
|
if (import.meta.client) {
|
|
useResizeObserver(scrollableControlsContainer, (entries) => {
|
|
const { height: newHeight } = entries[0].contentRect
|
|
height.value = newHeight
|
|
})
|
|
useEventListener(resizeHandle, 'mousedown', startResizing)
|
|
|
|
useEventListener(document, 'mousemove', (event) => {
|
|
if (isResizing.value) {
|
|
const diffX = event.clientX - startX
|
|
const newWidth = Math.max(
|
|
240,
|
|
Math.min(startWidth + diffX, Math.min(440, window.innerWidth * 0.5 - 60))
|
|
)
|
|
width.value = newWidth
|
|
}
|
|
})
|
|
|
|
useEventListener(document, 'mouseup', () => {
|
|
if (isResizing.value) {
|
|
isResizing.value = false
|
|
}
|
|
})
|
|
}
|
|
|
|
const { isIntercomEnabled } = useIntercomEnabled()
|
|
const { resourceItems, modelsAndVersionIds } = useInjectedViewerLoadedResources()
|
|
const { registerShortcuts, getShortcutDisplayText, shortcuts } = useViewerShortcuts()
|
|
const { isEnabled: isEmbedEnabled } = useEmbed()
|
|
const breakpoints = useBreakpoints(TailwindBreakpoints)
|
|
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
|
|
const isMobile = breakpoints.smaller('sm')
|
|
const isTablet = breakpoints.smaller('lg')
|
|
const { getTooltipProps } = useSmartTooltipDelay()
|
|
const isSavedViewsEnabled = useAreSavedViewsEnabled()
|
|
const { $intercom } = useNuxtApp()
|
|
const { hasActiveFilters } = useFilterUtilities()
|
|
|
|
const activePanel = ref<ActivePanel>('none')
|
|
const modelsSubView = ref<ModelsSubView>(ModelsSubView.Main)
|
|
|
|
const hasActivePanel = computed(() => activePanel.value !== 'none')
|
|
|
|
const allAutomationRuns = computed(() => {
|
|
const allAutomationStatuses = modelsAndVersionIds.value
|
|
.map(({ model }) => model.loadedVersion.items[0].automationsStatus)
|
|
.flat()
|
|
.filter(isNonNullable)
|
|
|
|
return allAutomationStatuses.map((status) => status.automationRuns).flat()
|
|
})
|
|
|
|
const allFunctionRuns = computed(() => {
|
|
return allAutomationRuns.value.map((run) => run.functionRuns).flat()
|
|
})
|
|
|
|
const widthClass = computed(() => {
|
|
if (isMobile.value) {
|
|
return 'calc(100vw - 3.6rem)'
|
|
} else if (isTablet.value) {
|
|
return '240px'
|
|
} else {
|
|
return `${width.value + 4}px`
|
|
}
|
|
})
|
|
|
|
const { summary } = useFunctionRunsStatusSummary({
|
|
runs: allFunctionRuns
|
|
})
|
|
|
|
registerShortcuts({
|
|
ToggleModels: () => toggleActivePanel('models'),
|
|
ToggleFilters: () => toggleActivePanel('filters'),
|
|
ToggleDiscussions: () => toggleActivePanel('discussions'),
|
|
ToggleDevMode: () => toggleActivePanel('devMode'),
|
|
ToggleSavedViews: () => isSavedViewsEnabled && toggleActivePanel('savedViews')
|
|
})
|
|
|
|
const toggleActivePanel = (panel: ActivePanel) => {
|
|
const wasNone = activePanel.value === 'none'
|
|
|
|
if (panel === 'models') {
|
|
if (activePanel.value === 'models') {
|
|
if (
|
|
modelsSubView.value === ModelsSubView.Versions ||
|
|
modelsSubView.value === ModelsSubView.Diff
|
|
) {
|
|
// Go back to main models view instead of closing
|
|
modelsSubView.value = ModelsSubView.Main
|
|
return
|
|
} else {
|
|
activePanel.value = 'none'
|
|
}
|
|
} else {
|
|
// Open models panel and reset to main view
|
|
activePanel.value = 'models'
|
|
modelsSubView.value = ModelsSubView.Main
|
|
}
|
|
} else {
|
|
activePanel.value = activePanel.value === panel ? 'none' : panel
|
|
modelsSubView.value = ModelsSubView.Main
|
|
}
|
|
|
|
// If a panel is being opened (not closed) on mobile, emit event to parent
|
|
if (wasNone && activePanel.value !== 'none' && isMobile.value) {
|
|
emit('forceClosePanels')
|
|
}
|
|
}
|
|
|
|
const forceClosePanel = () => {
|
|
activePanel.value = 'none'
|
|
}
|
|
|
|
const openDocs = () => {
|
|
window.open(viewerDocsRoute, '_blank')
|
|
}
|
|
|
|
const openIntercomChat = () => {
|
|
if (isIntercomEnabled.value) {
|
|
$intercom.show()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
activePanel.value =
|
|
isSmallerOrEqualSm.value || isEmbedEnabled.value ? 'none' : 'models'
|
|
})
|
|
|
|
watch(isSmallerOrEqualSm, (newVal) => {
|
|
activePanel.value = newVal ? 'none' : 'models'
|
|
})
|
|
|
|
defineExpose({
|
|
forceClosePanel,
|
|
forceClosePanels: forceClosePanel
|
|
})
|
|
</script>
|