refactor(fe): Models panel with versions and diff

refactor(fe): Models panel with versions and diff
This commit is contained in:
andrewwallacespeckle
2025-08-13 09:53:18 +01:00
committed by GitHub
4 changed files with 113 additions and 85 deletions
@@ -133,7 +133,7 @@
:style="`width: ${widthClass};`"
>
<KeepAlive v-show="activePanel === 'models'">
<ViewerModelsPanel />
<ViewerModelsPanel v-model:sub-view="modelsSubView" />
</KeepAlive>
<KeepAlive v-show="resourceItems.length !== 0 && activePanel === 'filters'">
<ViewerFiltersPanel />
@@ -167,6 +167,7 @@ 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'
@@ -237,6 +238,7 @@ const { $intercom } = useNuxtApp()
const { hasActiveFilters } = useFilterUtilities()
const activePanel = ref<ActivePanel>('none')
const modelsSubView = ref<ModelsSubView>(ModelsSubView.Main)
const hasActivePanel = computed(() => activePanel.value !== 'none')
@@ -277,7 +279,28 @@ registerShortcuts({
const toggleActivePanel = (panel: ActivePanel) => {
const wasNone = activePanel.value === 'none'
activePanel.value = activePanel.value === panel ? 'none' : panel
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) {
@@ -1,7 +1,12 @@
<template>
<div class="select-none h-full">
<ViewerCompareChangesPanel
v-if="subView === 'diff'"
:clear-on-back="false"
@close="handleDiffClose"
/>
<ViewerModelsVersions
v-if="showVersions"
v-else-if="subView === 'versions'"
:expanded-model-id="expandedModelId"
@close="handleVersionsClose"
/>
@@ -15,7 +20,7 @@
<ViewerModelsActions
v-if="!hasObjects"
:hide-versions="resourceItems.length === 0 && objects.length === 0"
@show-versions="showVersions = true"
@show-versions="subView = ModelsSubView.Versions"
@add-model="showAddModel = true"
/>
</template>
@@ -106,7 +111,7 @@ import {
useInjectedViewerState
} from '~~/lib/viewer/composables/setup'
import type { ExplorerNode } from '~~/lib/viewer/helpers/sceneExplorer'
import { ModelsSubView, type ExplorerNode } from '~~/lib/viewer/helpers/sceneExplorer'
import type { ViewerLoadedResourcesQuery } from '~~/lib/common/generated/gql/graphql'
import type { Get } from 'type-fest'
import { useDiffUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
@@ -118,12 +123,11 @@ import { useVirtualList, useDebounceFn } from '@vueuse/core'
type ModelItem = NonNullable<Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>>
defineEmits(['close'])
const showVersions = ref(false)
const showAddModel = ref(false)
const subView = defineModel<ModelsSubView>('subView', { default: ModelsSubView.Main })
const expandedModelId = ref<string | null>(null)
const showAddModel = ref(false)
const expandedNodes = ref<Set<string>>(new Set())
const expandedModels = ref<Set<string>>(new Set())
const disableScrollOnNextSelection = ref(false)
@@ -139,7 +143,8 @@ const {
const {
resources: {
response: { resourceItems: stateResourceItems }
}
},
ui: { diff: diffState }
} = useInjectedViewerState()
const {
objects: selectedObjects,
@@ -147,7 +152,7 @@ const {
clearSelection,
removeFromSelection
} = useSelectionUtilities()
const { diffModelVersions } = useDiffUtilities()
const { diffModelVersions, endDiff } = useDiffUtilities()
const {
flattenModelTree,
getRootNodesForModel,
@@ -211,22 +216,31 @@ const modelHeaderPositions = computed(() => {
return headers
})
const hasDiffActive = computed(() => {
return !!(diffState.oldVersion.value && diffState.newVersion.value)
})
const handleShowVersions = (modelId: string) => {
expandedModelId.value = modelId
showVersions.value = true
subView.value = ModelsSubView.Versions
}
const handleShowDiff = async (modelId: string, versionA: string, versionB: string) => {
await diffModelVersions(modelId, versionA, versionB)
expandedModelId.value = modelId
showVersions.value = true
subView.value = ModelsSubView.Diff
}
const handleVersionsClose = () => {
showVersions.value = false
subView.value = ModelsSubView.Main
expandedModelId.value = null
}
const handleDiffClose = async () => {
await endDiff()
subView.value = ModelsSubView.Versions
}
const toggleModelExpansion = (modelId: string) => {
if (expandedModels.value.has(modelId)) {
expandedModels.value.delete(modelId)
@@ -383,6 +397,18 @@ const handleScroll = (e: Event) => {
watch(selectedObjects, handleSelectionChange, { deep: true })
watch(subView, (newSubView) => {
if (newSubView === ModelsSubView.Main) {
expandedModelId.value = null
}
})
watch(hasDiffActive, (isActive) => {
if (isActive && subView.value !== ModelsSubView.Diff) {
subView.value = ModelsSubView.Diff
}
})
// Initialize and update sticky header when models change
watch(
unifiedVirtualItems,
@@ -1,58 +1,51 @@
<template>
<div class="h-full">
<ViewerCompareChangesPanel
v-if="showDiff"
:clear-on-back="false"
@close="handleDiffClose"
/>
<ViewerLayoutSidePanel v-else>
<template #title>
<div class="flex items-center gap-x-1">
<FormButton
:icon-left="ChevronLeftIcon"
color="subtle"
class="-ml-3"
hide-text
size="sm"
@click="handleClose"
<ViewerLayoutSidePanel>
<template #title>
<div class="flex items-center gap-x-1">
<FormButton
:icon-left="ChevronLeftIcon"
color="subtle"
class="-ml-3"
hide-text
size="sm"
@click="handleClose"
>
Exit versions
</FormButton>
Versions
</div>
</template>
<div class="flex flex-col h-full">
<template v-if="resourceItems.length">
<!-- Versions with single scroll container for sticky headers -->
<div class="flex-1 overflow-y-auto simple-scrollbar">
<div
v-for="({ model, versionId }, index) in modelsAndVersionIds"
:key="model.id"
>
Exit versions
</FormButton>
Versions
<ViewerModelsVersionsCard
:model="model"
:version-id="versionId"
:last="index === modelsAndVersionIds.length - 1"
:initially-expanded="
props.expandedModelId === model.id || modelsAndVersionIds.length === 1
"
/>
</div>
<template v-if="objects.length !== 0">
<ViewerResourcesObjectCard
v-for="object in objects"
:key="object.objectId"
:object="object"
:show-remove="false"
@remove="(id: string) => removeModel(id)"
/>
</template>
</div>
</template>
<div class="flex flex-col h-full">
<template v-if="resourceItems.length">
<!-- Versions with single scroll container for sticky headers -->
<div class="flex-1 overflow-y-auto simple-scrollbar">
<div
v-for="({ model, versionId }, index) in modelsAndVersionIds"
:key="model.id"
>
<ViewerModelsVersionsCard
:model="model"
:version-id="versionId"
:last="index === modelsAndVersionIds.length - 1"
:initially-expanded="
props.expandedModelId === model.id || modelsAndVersionIds.length === 1
"
/>
</div>
<template v-if="objects.length !== 0">
<ViewerResourcesObjectCard
v-for="object in objects"
:key="object.objectId"
:object="object"
:show-remove="false"
@remove="(id: string) => removeModel(id)"
/>
</template>
</div>
</template>
</div>
</ViewerLayoutSidePanel>
</div>
</div>
</ViewerLayoutSidePanel>
</template>
<script setup lang="ts">
@@ -84,19 +77,10 @@ const { endDiff } = useDiffUtilities()
const mp = useMixpanel()
const showDiff = ref(false)
const hasDiffActive = computed(() => {
return !!(diffState.oldVersion.value && diffState.newVersion.value)
})
const handleDiffClose = async () => {
showDiff.value = false
if (hasDiffActive.value) {
await endDiff()
}
}
const handleClose = async () => {
if (hasDiffActive.value) {
await endDiff()
@@ -129,15 +113,4 @@ const refhack = ref(1)
useViewerEventListener(ViewerEvent.LoadComplete, () => {
refhack.value++
})
// Watch for diff becoming active and show it
watch(
hasDiffActive,
(newVal) => {
if (newVal) {
showDiff.value = true
}
},
{ immediate: true }
)
</script>
@@ -32,3 +32,9 @@ export type TreeItemComponentModel = {
}
export type { SpeckleObject, SpeckleReference }
export enum ModelsSubView {
Main = 'main',
Versions = 'versions',
Diff = 'diff'
}