Files
speckle-server/packages/frontend-2/components/viewer/resources/VersionCard.vue
T
2025-08-12 12:47:03 +01:00

249 lines
7.1 KiB
Vue

<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<div
class="group relative w-full rounded-md pb-2 text-left pl-5 pt-2"
:class="
clickable && !isLimited ? 'hover:bg-highlight-1 cursor-pointer' : 'cursor-default'
"
@click="handleClick"
@keypress="keyboardClick(handleClick)"
>
<!-- Timeline left border -->
<div
v-if="showTimeline"
class="absolute top-5 left-4 z-10 ml-[2px] w-1 border-l border-outline-3"
:class="last ? 'h-0' : 'h-[99%]'"
>
<div
v-if="isLoaded"
class="absolute -top-2.5 -left-2 flex items-center justify-center h-4 w-4 bg-foundation-2 rounded-full"
>
<IconCheck class="h-4 w-4 text-foreground" />
</div>
<div
v-else
class="absolute top-0 -left-[2px] h-[3px] w-[3px] bg-foreground rounded-full"
/>
</div>
<div class="flex items-center gap-1">
<div
v-show="showTimeline"
v-tippy="createdAt.full"
class="rounded-full px-2 text-body-xs font-medium ml-3"
>
<span>
{{ isLatest ? 'Latest' : createdAt.relative }}
</span>
</div>
<CommonBadge v-if="isLoaded" rounded>Viewing</CommonBadge>
<LayoutMenu
v-model:open="showActionsMenu"
class="ml-auto mr-2"
:items="actionsItems"
:menu-position="HorizontalDirection.Left"
mount-menu-on-body
@click.stop.prevent
@chosen="onActionChosen"
>
<button
class="opacity-0 group-hover:opacity-100 hover:bg-highlight-3 rounded-md h-5 w-5 flex items-center justify-center shrink-0"
@click.stop="showActionsMenu = !showActionsMenu"
>
<IconThreeDots />
</button>
</LayoutMenu>
</div>
<!-- Main stuff -->
<div class="flex items-center pl-5 gap-2 mt-2">
<div
class="bg-foundation h-12 w-12 flex-shrink-0 rounded-md border border-outline-3"
:class="isLimited ? 'diagonal-stripes' : ''"
>
<div v-if="isLimited" class="flex items-center justify-center w-full h-full">
<div
class="flex h-8 w-8 items-center justify-center rounded-md bg-foundation border border-outline-3"
>
<LockClosedIcon class="h-4 w-4 text-foreground-3" />
</div>
</div>
<PreviewImage v-else :preview-url="version.previewUrl" />
</div>
<div class="flex flex-col space-y-1 overflow-hidden">
<div class="flex min-w-0 items-center space-x-1">
<ViewerResourcesLimitAlert
v-if="isLimited"
limit-type="version"
variant="inline"
:project="project"
/>
<div v-else class="truncate pr-2">
<div v-if="author" class="text-body-2xs truncate">
{{ author.name }}
</div>
<div class="text-body-3xs text-foreground-2 truncate">
{{ version.message || 'no message' }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { LockClosedIcon } from '@heroicons/vue/24/solid'
import { CommonBadge, keyboardClick } from '@speckle/ui-components'
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import type { ViewerModelVersionCardItemFragment } from '~~/lib/common/generated/gql/graphql'
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
import { HorizontalDirection } from '~~/lib/common/composables/window'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useCopyModelLink } from '~/lib/projects/composables/modelManagement'
dayjs.extend(localizedFormat)
const props = withDefaults(
defineProps<{
version: ViewerModelVersionCardItemFragment
clickable?: boolean
isLatestVersion: boolean
isLoadedVersion: boolean
showTimeline?: boolean
last: boolean
lastLoaded: boolean
modelId?: string
totalVersions?: number
}>(),
{
clickable: true,
default: false,
showTimeline: true,
last: false,
lastLoaded: false
}
)
const emit = defineEmits<{
(e: 'changeVersion', version: string): void
(e: 'viewChanges', version: ViewerModelVersionCardItemFragment): void
(e: 'removeVersion', versionId: string): void
}>()
const mp = useMixpanel()
const {
resources: {
response: { project }
}
} = useInjectedViewerState()
const copyModelLink = useCopyModelLink()
const IconThreeDots = resolveComponent('IconThreeDots')
const isLoaded = computed(() => props.isLoadedVersion)
const isLatest = computed(() => props.isLatestVersion)
// Check if version is limited by plan restrictions
const isLimited = computed(() => {
return props.version.referencedObject === null
})
const createdAt = computed(() => {
return {
full: formattedFullDate(props.version.createdAt),
relative: formattedRelativeDate(props.version.createdAt, { capitalize: true })
}
})
const author = computed(() => props.version.authorUser)
const IconCheck = resolveComponent('IconCheck')
const showActionsMenu = ref(false)
const canDeleteVersion = computed(() => {
if (isLoaded.value) return false
if (props.totalVersions && props.totalVersions <= 1) return false
return true
})
const deleteDisabledReason = computed(() => {
if (isLoaded.value) {
return 'Cannot delete the currently viewed version'
}
if (props.totalVersions && props.totalVersions <= 1) {
return 'Cannot delete the last version'
}
return undefined
})
const actionsItems = computed<LayoutMenuItem[][]>(() => [
[
{
title: 'View changes',
id: 'view-changes',
disabled: isLoaded.value || isLimited.value,
disabledTooltip: isLoaded.value
? 'Cannot compare current version with itself'
: isLimited.value
? 'Version comparison unavailable'
: undefined
},
{
title: 'Copy link to version',
id: 'copy-link-to-version',
disabled: isLimited.value,
disabledTooltip: isLimited.value ? 'Outside workspace version limits' : undefined
}
],
[
{
title: 'Remove version',
id: 'remove-version',
disabled: !canDeleteVersion.value,
disabledTooltip: deleteDisabledReason.value
}
]
])
const handleClick = () => {
if (isLimited.value) return
if (props.clickable) emit('changeVersion', props.version.id)
mp.track('Viewer Action', {
type: 'action',
name: 'change-version'
})
}
const handleViewChanges = () => {
emit('viewChanges', props.version)
mp.track('Viewer Action', {
type: 'action',
name: 'diffs',
action: 'enable'
})
}
const onActionChosen = (params: { item: LayoutMenuItem }) => {
const { item } = params
switch (item.id) {
case 'view-changes':
if (!isLoaded.value && !isLimited.value) {
handleViewChanges()
}
break
case 'copy-link-to-version':
if (project.value?.id && props.modelId) {
copyModelLink(project.value.id, props.modelId, props.version.id)
}
break
case 'remove-version':
if (canDeleteVersion.value) {
emit('removeVersion', props.version.id)
}
break
}
}
</script>