Versions panel
This commit is contained in:
@@ -10,24 +10,9 @@
|
||||
Versions
|
||||
</FormButton>
|
||||
</div>
|
||||
<div
|
||||
v-tippy="removeEnabled ? 'Remove model' : 'You cannot remove the last model'"
|
||||
class="flex"
|
||||
>
|
||||
<FormButton
|
||||
color="subtle"
|
||||
:disabled="!removeEnabled"
|
||||
:icon-left="IconMinus"
|
||||
hide-text
|
||||
@click="$emit('remove')"
|
||||
>
|
||||
{{ showRemove ? 'Done' : 'Remove' }}
|
||||
</FormButton>
|
||||
</div>
|
||||
<div v-tippy="'Add model'" class="flex">
|
||||
<FormButton
|
||||
color="subtle"
|
||||
:disabled="showRemove"
|
||||
hide-text
|
||||
:icon-left="IconPlus"
|
||||
@click="$emit('addModel')"
|
||||
@@ -39,20 +24,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
removeEnabled: boolean
|
||||
showRemove: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
defineEmits<{
|
||||
showVersions: []
|
||||
remove: []
|
||||
addModel: []
|
||||
}>()
|
||||
|
||||
const IconPlus = resolveComponent('IconPlus')
|
||||
const IconVersions = resolveComponent('IconVersions')
|
||||
const IconMinus = resolveComponent('IconMinus')
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<template>
|
||||
<div class="relative border-b border-outline-3">
|
||||
<div
|
||||
:class="showRemove ? 'pointer-events-none' : ''"
|
||||
@mouseenter="highlightObject"
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusin="highlightObject"
|
||||
@@ -13,14 +12,9 @@
|
||||
<!-- Model Header -->
|
||||
<div
|
||||
class="group flex items-center px-1 py-3 select-none cursor-pointer hover:bg-highlight-1"
|
||||
:class="isExpanded && !showRemove ? 'border-b border-outline-3' : ''"
|
||||
:class="isExpanded ? 'border-b border-outline-3' : ''"
|
||||
>
|
||||
<FormButton
|
||||
v-if="!showRemove"
|
||||
size="sm"
|
||||
color="subtle"
|
||||
@click.stop="isExpanded = !isExpanded"
|
||||
>
|
||||
<FormButton size="sm" color="subtle" @click.stop="isExpanded = !isExpanded">
|
||||
<IconTriangle
|
||||
class="w-4 h-4 -ml-1.5 -mr-1.5 text-foreground-2"
|
||||
:class="isExpanded ? 'rotate-90' : ''"
|
||||
@@ -29,7 +23,6 @@
|
||||
{{ isExpanded ? 'Collapse' : 'Expand' }}
|
||||
</span>
|
||||
</FormButton>
|
||||
<div v-else class="w-2" />
|
||||
<div class="h-12 w-12 rounded-md overflow-hidden border border-outline-3 mr-3">
|
||||
<NuxtImg
|
||||
:src="loadedVersion?.previewUrl"
|
||||
@@ -52,6 +45,22 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<div class="flex text-foreground">
|
||||
<LayoutMenu
|
||||
v-model:open="showActionsMenu"
|
||||
:items="actionsItems"
|
||||
:menu-position="HorizontalDirection.Left"
|
||||
mount-menu-on-body
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
>
|
||||
<FormButton
|
||||
color="subtle"
|
||||
class="group-hover:opacity-100 opacity-0"
|
||||
hide-text
|
||||
:icon-right="EllipsisHorizontalIcon"
|
||||
@click="showActionsMenu = !showActionsMenu"
|
||||
/>
|
||||
</LayoutMenu>
|
||||
<FormButton
|
||||
color="subtle"
|
||||
class="group-hover:opacity-100"
|
||||
@@ -84,7 +93,7 @@
|
||||
|
||||
<!-- Scene Explorer Content -->
|
||||
<div
|
||||
v-if="isExpanded && rootNodeChildren.length && !showRemove"
|
||||
v-if="isExpanded && rootNodeChildren.length"
|
||||
class="relative flex flex-col gap-y-2"
|
||||
>
|
||||
<div v-for="(childNode, idx) in rootNodeChildren" :key="idx" class="rounded-xl">
|
||||
@@ -98,41 +107,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remove Overlay -->
|
||||
<div
|
||||
v-if="showRemove"
|
||||
class="absolute top-0 right-2 h-full z-10 flex items-center justify-end"
|
||||
>
|
||||
<FormButton
|
||||
color="danger"
|
||||
size="sm"
|
||||
hide-text
|
||||
:icon-left="XMarkIcon"
|
||||
@click="$emit('remove', props.model.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { XMarkIcon, FunnelIcon } from '@heroicons/vue/24/solid'
|
||||
import { FunnelIcon, EllipsisHorizontalIcon } from '@heroicons/vue/24/solid'
|
||||
import { FunnelIcon as FunnelIconOutline } from '@heroicons/vue/24/outline'
|
||||
import type { ViewerLoadedResourcesQuery } from '~~/lib/common/generated/gql/graphql'
|
||||
import type { Get } from 'type-fest'
|
||||
import type { ExplorerNode } from '~~/lib/viewer/helpers/sceneExplorer'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
import { HorizontalDirection } from '~~/lib/common/composables/window'
|
||||
import {
|
||||
useHighlightedObjectsUtilities,
|
||||
useFilterUtilities,
|
||||
useSelectionUtilities
|
||||
} from '~~/lib/viewer/composables/ui'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import {
|
||||
useInjectedViewerState,
|
||||
useInjectedViewerRequestedResources
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import { containsAll } from '~~/lib/common/helpers/utils'
|
||||
|
||||
type ModelItem = NonNullable<Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>>
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
(e: 'remove', val: string): void
|
||||
(e: 'expanded', depth: number): void
|
||||
}>()
|
||||
@@ -140,7 +140,6 @@ defineEmits<{
|
||||
const props = defineProps<{
|
||||
model: ModelItem
|
||||
versionId: string
|
||||
showRemove: boolean
|
||||
last: boolean
|
||||
expandLevel: number
|
||||
manualExpandLevel: number
|
||||
@@ -151,6 +150,7 @@ const { highlightObjects, unhighlightObjects } = useHighlightedObjectsUtilities(
|
||||
const { hideObjects, showObjects, isolateObjects, unIsolateObjects } =
|
||||
useFilterUtilities()
|
||||
const { setSelectionFromObjectIds } = useSelectionUtilities()
|
||||
const { items } = useInjectedViewerRequestedResources()
|
||||
const {
|
||||
viewer: {
|
||||
metadata: { filteringState }
|
||||
@@ -158,10 +158,24 @@ const {
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const showActionsMenu = ref(false)
|
||||
|
||||
const IconEye = resolveComponent('IconEye')
|
||||
const IconEyeClosed = resolveComponent('IconEyeClosed')
|
||||
|
||||
const removeEnabled = computed(() => items.value.length > 1)
|
||||
|
||||
const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
title: 'Remove model',
|
||||
id: 'remove-model',
|
||||
disabled: !removeEnabled.value,
|
||||
disabledTooltip: 'You cannot remove the last model'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const rootNodeChildren = computed(() => {
|
||||
const children: ExplorerNode[] = []
|
||||
for (const rootNode of props.rootNodes) {
|
||||
@@ -272,4 +286,16 @@ const selectObject = () => {
|
||||
if (modelObjectIds.value.length === 0) return
|
||||
setSelectionFromObjectIds(modelObjectIds.value)
|
||||
}
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem }) => {
|
||||
const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case 'remove-model':
|
||||
if (removeEnabled.value) {
|
||||
emit('remove', props.model.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="select-none">
|
||||
<ViewerModelsVersions v-if="showVersions" @close="showVersions = false" />
|
||||
<ViewerModelsAddPanel v-else-if="showAddModel" @close="showAddModel = false" />
|
||||
<ViewerLayoutSidePanel v-else>
|
||||
<template #title>
|
||||
<FormButton
|
||||
v-if="showRemove"
|
||||
:icon-left="ChevronLeftIcon"
|
||||
color="subtle"
|
||||
class="-ml-3"
|
||||
@click="showRemove = false"
|
||||
>
|
||||
Exit
|
||||
</FormButton>
|
||||
<span v-else>Models</span>
|
||||
<span>Models</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ViewerModelsActions
|
||||
v-if="!showRemove"
|
||||
:remove-enabled="removeEnabled"
|
||||
:show-remove="showRemove"
|
||||
@show-versions="showVersions = true"
|
||||
@remove="handleRemove()"
|
||||
@add-model="showAddModel = true"
|
||||
/>
|
||||
</template>
|
||||
@@ -37,7 +24,6 @@
|
||||
:model="model"
|
||||
:version-id="versionId"
|
||||
:last="index === modelsAndVersionIds.length - 1"
|
||||
:show-remove="showRemove"
|
||||
:expand-level="expandLevel"
|
||||
:manual-expand-level="manualExpandLevel"
|
||||
:root-nodes="getRootNodesForModel(model.id)"
|
||||
@@ -50,7 +36,7 @@
|
||||
v-for="object in objects"
|
||||
:key="object.objectId"
|
||||
:object="object"
|
||||
:show-remove="showRemove"
|
||||
:show-remove="false"
|
||||
@remove="(id: string) => removeModel(id)"
|
||||
/>
|
||||
</template>
|
||||
@@ -84,11 +70,9 @@ import { ViewerEvent } from '@speckle/viewer'
|
||||
import { useViewerEventListener } from '~~/lib/viewer/composables/viewer'
|
||||
import type { ExplorerNode } from '~~/lib/viewer/helpers/sceneExplorer'
|
||||
import { sortBy, flatten } from 'lodash-es'
|
||||
import { ChevronLeftIcon } from '@heroicons/vue/24/solid'
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const showRemove = ref(false)
|
||||
const showVersions = ref(false)
|
||||
const showAddModel = ref(false)
|
||||
const { resourceItems, modelsAndVersionIds, objects } =
|
||||
@@ -109,8 +93,6 @@ const showRaw = ref(false)
|
||||
|
||||
const mp = useMixpanel()
|
||||
|
||||
const removeEnabled = computed(() => items.value.length > 1)
|
||||
|
||||
const removeModel = async (modelId: string) => {
|
||||
// Convert requested resource string to references to specific models
|
||||
// to ensure remove works even when we have "all" or "$folder" in the URL
|
||||
@@ -194,12 +176,4 @@ const getRootNodesForModel = (modelId: string) => {
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
const handleRemove = () => {
|
||||
showRemove.value = true
|
||||
}
|
||||
|
||||
watch(modelsAndVersionIds, (newVal) => {
|
||||
if (newVal.length <= 1) showRemove.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
|
||||
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
||||
<template>
|
||||
<div class="relative border-b border-outline-3">
|
||||
<div
|
||||
class="relative border-b border-outline-3"
|
||||
:class="showVersions ? 'bg-foundation-page' : 'bg-foundation hover:bg-highlight-1'"
|
||||
>
|
||||
<div
|
||||
:class="showVersions ? 'max-h-96 shadow-md' : ''"
|
||||
:class="showVersions ? 'max-h-96' : ''"
|
||||
@mouseenter="highlightObject"
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusin="highlightObject"
|
||||
@@ -11,77 +14,40 @@
|
||||
>
|
||||
<!-- Model Header -->
|
||||
<div
|
||||
class="group sticky cursor-pointer top-0 z-20 flex min-w-0 max-w-full items-center justify-between gap-3 pl-1 pr-4 py-2 select-none"
|
||||
:class="showVersions ? 'bg-primary' : 'bg-foundation hover:bg-foundation-2'"
|
||||
class="group sticky cursor-pointer flex items-center gap-3 py-2 px-1"
|
||||
@click="showVersions = !showVersions"
|
||||
>
|
||||
<div class="h-12 w-12 rounded-md overflow-hidden border border-outline-3">
|
||||
<NuxtImg
|
||||
:src="loadedVersion?.previewUrl"
|
||||
class="object-cover h-full w-full"
|
||||
<PreviewImage
|
||||
v-if="loadedVersion?.previewUrl"
|
||||
:preview-url="loadedVersion?.previewUrl"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-grow flex-col">
|
||||
<div
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
v-tippy="modelName.subheader ? model.name : null"
|
||||
:class="`${
|
||||
showVersions ? 'text-foundation' : ''
|
||||
} text-body-xs truncate min-w-0`"
|
||||
class="text-foreground text-body-2xs font-medium"
|
||||
>
|
||||
{{ modelName.header }}
|
||||
</div>
|
||||
<div class="truncate -mt-1.5">
|
||||
<span
|
||||
v-tippy="createdAtFormatted.full"
|
||||
:class="`${
|
||||
showVersions ? 'text-foundation' : 'text-foreground-2'
|
||||
} text-body-3xs`"
|
||||
>
|
||||
{{ isLatest ? 'Latest version' : createdAtFormatted.relative }}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="isLatest" class="text-body-3xs text-foreground">
|
||||
Latest version
|
||||
</span>
|
||||
<span
|
||||
v-tippy="createdAtFormatted.full"
|
||||
class="text-body-3xs text-foreground-2"
|
||||
>
|
||||
{{ createdAtFormatted.relative }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="!showVersions" class="text-foreground-2 text-body-2xs font-medium">
|
||||
<span class="text-foreground-2 text-body-3xs font-medium ml-auto pr-3">
|
||||
{{ model.versions?.totalCount }}
|
||||
</span>
|
||||
<div
|
||||
v-else
|
||||
:class="`${
|
||||
showVersions ? 'text-white' : ''
|
||||
} flex flex-none items-center space-x-2 text-xs font-medium opacity-80 transition-opacity group-hover:opacity-100`"
|
||||
>
|
||||
<ChevronUpIcon class="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Version Card (when expanded but no scene data) -->
|
||||
<ViewerModelsActiveVersionCard
|
||||
v-if="loadedVersion && showVersions"
|
||||
:version="loadedVersion"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Remove Overlay -->
|
||||
<Transition>
|
||||
<div
|
||||
v-if="showRemove"
|
||||
class="to-foundation group absolute inset-0 z-[21] flex h-full w-full items-center justify-end space-x-2 rounded bg-gradient-to-r from-blue-500/0 p-4"
|
||||
>
|
||||
<FormButton
|
||||
color="danger"
|
||||
size="sm"
|
||||
hide-text
|
||||
:icon-left="XMarkIcon"
|
||||
@click="$emit('remove', props.model.id)"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Version List (when expanded and not in remove mode) -->
|
||||
<div
|
||||
v-show="showVersions && !showRemove"
|
||||
class="mt-2 ml-4 flex h-auto flex-col space-y-0"
|
||||
>
|
||||
<!-- Version List -->
|
||||
<div v-show="showVersions" class="mt-2 ml-4 flex h-auto flex-col space-y-0">
|
||||
<ViewerResourcesVersionCard
|
||||
v-for="(version, index) in props.model.versions.items"
|
||||
:key="version.id"
|
||||
@@ -92,22 +58,39 @@
|
||||
:last="index === props.model.versions.totalCount - 1"
|
||||
:last-loaded="index === props.model.versions.items.length - 1"
|
||||
:clickable="version.id !== loadedVersion?.id"
|
||||
:total-versions="props.model.versions.totalCount"
|
||||
@change-version="handleVersionChange"
|
||||
@view-changes="handleViewChanges"
|
||||
@remove-version="handleRemoveVersion"
|
||||
/>
|
||||
<div class="mt-4 px-2 py-2">
|
||||
<FormButton full-width text :disabled="!showLoadMore" @click="onLoadMore">
|
||||
<div class="mt-4 pr-2 py-2 -ml-3">
|
||||
<FormButton
|
||||
full-width
|
||||
text
|
||||
color="subtle"
|
||||
:disabled="!showLoadMore"
|
||||
@click="onLoadMore"
|
||||
>
|
||||
{{ showLoadMore ? 'View older versions' : 'No more versions' }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Delete Dialog -->
|
||||
<ProjectModelPageDialogDelete
|
||||
v-if="project?.id"
|
||||
v-model:open="showDeleteDialog"
|
||||
:project-id="project.id"
|
||||
:model-id="model.id"
|
||||
:versions="versionsToDelete"
|
||||
@deleted="onVersionDeleted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { XMarkIcon, ChevronUpIcon } from '@heroicons/vue/24/solid'
|
||||
import type {
|
||||
ViewerLoadedResourcesQuery,
|
||||
ViewerModelVersionCardItemFragment
|
||||
@@ -115,7 +98,8 @@ import type {
|
||||
import type { Get } from 'type-fest'
|
||||
import {
|
||||
useInjectedViewerLoadedResources,
|
||||
useInjectedViewerRequestedResources
|
||||
useInjectedViewerRequestedResources,
|
||||
useInjectedViewerState
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import {
|
||||
useDiffUtilities,
|
||||
@@ -124,14 +108,9 @@ import {
|
||||
|
||||
type ModelItem = NonNullable<Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>>
|
||||
|
||||
defineEmits<{
|
||||
(e: 'remove', val: string): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
model: ModelItem
|
||||
versionId: string
|
||||
showRemove: boolean
|
||||
last: boolean
|
||||
}>()
|
||||
|
||||
@@ -139,8 +118,15 @@ const { switchModelToVersion } = useInjectedViewerRequestedResources()
|
||||
const { loadMoreVersions } = useInjectedViewerLoadedResources()
|
||||
const { diffModelVersions } = useDiffUtilities()
|
||||
const { highlightObjects, unhighlightObjects } = useHighlightedObjectsUtilities()
|
||||
const {
|
||||
resources: {
|
||||
response: { project }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const showVersions = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const versionsToDelete = ref<{ id: string; message?: string | null }[]>([])
|
||||
|
||||
graphql(`
|
||||
fragment ViewerModelVersionCardItem on Version {
|
||||
@@ -229,4 +215,26 @@ const unhighlightObject = () => {
|
||||
const refObject = props.model.loadedVersion.items[0]?.referencedObject
|
||||
if (refObject) unhighlightObjects([refObject])
|
||||
}
|
||||
|
||||
const handleRemoveVersion = (versionId: string) => {
|
||||
// Find the version to delete
|
||||
const versionToDelete = versions.value.find((v) => v.id === versionId)
|
||||
if (versionToDelete) {
|
||||
versionsToDelete.value = [
|
||||
{ id: versionToDelete.id, message: versionToDelete.message }
|
||||
]
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const onVersionDeleted = () => {
|
||||
// Refresh the versions list after successful deletion
|
||||
loadMoreVersions(props.model.id)
|
||||
}
|
||||
|
||||
watch(showDeleteDialog, (isOpen) => {
|
||||
if (!isOpen) {
|
||||
versionsToDelete.value = []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
:model="model"
|
||||
:version-id="versionId"
|
||||
:last="index === modelsAndVersionIds.length - 1"
|
||||
:show-remove="false"
|
||||
@remove="(id: string) => removeModel(id)"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="objects.length !== 0">
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
||||
<template>
|
||||
<div
|
||||
:class="`group relative block w-full space-y-2 rounded-md pb-2 text-left ${
|
||||
class="group relative w-full rounded-md pb-2 text-left"
|
||||
:class="
|
||||
clickable && !isLimited
|
||||
? 'hover:bg-primary-muted cursor-pointer'
|
||||
: 'cursor-default'
|
||||
}
|
||||
${isLoaded ? 'bg-highlight-3' : 'bg-highlight-1'}
|
||||
`"
|
||||
"
|
||||
@click="handleClick"
|
||||
@keypress="keyboardClick(handleClick)"
|
||||
>
|
||||
<!-- Timeline left border -->
|
||||
<div
|
||||
v-if="showTimeline"
|
||||
:class="`absolute top-3 ml-[2px] h-[99%] w-1 ${
|
||||
isLoaded
|
||||
? 'border-primary border-r-4 border'
|
||||
: 'border-dashed border-outline-3 border-r-2'
|
||||
} group-hover:border-primary left-[7px] z-10`"
|
||||
></div>
|
||||
<div
|
||||
v-if="last"
|
||||
class="bg-primary absolute -bottom-5 ml-2 h-2 w-2 rounded-sm"
|
||||
></div>
|
||||
<div
|
||||
v-if="lastLoaded && !last"
|
||||
class="bg-primary absolute -bottom-6 z-10 ml-[4px] flex h-4 w-4 items-center justify-center rounded-full text-foreground-on-primary"
|
||||
class="absolute top-3 ml-[2px] h-[99%] w-1 border-l border-outline-3 group-hover:border-primary left-0 z-10"
|
||||
>
|
||||
<ChevronDownIcon class="h-3 w-3" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1 pl-1">
|
||||
<div class="z-20 -ml-2">
|
||||
<UserAvatar :user="author" />
|
||||
<div
|
||||
v-if="isLoaded"
|
||||
class="absolute -top-1.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
|
||||
v-if="last"
|
||||
class="absolute bottom-0 -left-[2px] h-[3px] w-[3px] bg-foreground rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 pl-1">
|
||||
<div
|
||||
v-show="showTimeline"
|
||||
v-tippy="createdAt.full"
|
||||
class="bg-foundation-focus inline-block rounded-full px-2 text-body-xs font-medium shrink-0"
|
||||
class="rounded-full px-2 text-body-xs font-medium ml-2.5"
|
||||
>
|
||||
<span>
|
||||
{{ isLatest ? 'Latest' : createdAt.relative }}
|
||||
@@ -54,14 +52,30 @@
|
||||
>
|
||||
View changes
|
||||
</FormButton>
|
||||
<FormButton v-else size="sm" text class="cursor-not-allowed">
|
||||
Currently viewing
|
||||
</FormButton>
|
||||
<CommonBadge v-else 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"
|
||||
>
|
||||
<FormButton
|
||||
color="subtle"
|
||||
class="opacity-0 group-hover:opacity-100"
|
||||
hide-text
|
||||
size="sm"
|
||||
:icon-right="EllipsisHorizontalIcon"
|
||||
@click.stop="showActionsMenu = !showActionsMenu"
|
||||
/>
|
||||
</LayoutMenu>
|
||||
</div>
|
||||
<!-- Main stuff -->
|
||||
<div class="flex items-center space-x-1 pl-5">
|
||||
<div
|
||||
class="bg-foundation h-16 w-16 flex-shrink-0 rounded-md border border-outline-3"
|
||||
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">
|
||||
@@ -81,27 +95,28 @@
|
||||
variant="inline"
|
||||
:project="project"
|
||||
/>
|
||||
<div v-else class="truncate text-xs">
|
||||
{{ version.message || 'no message' }}
|
||||
<div v-else class="truncate">
|
||||
<div v-if="author" class="text-body-2xs">
|
||||
{{ author.name }}
|
||||
</div>
|
||||
<div class="text-body-3xs text-foreground-2">
|
||||
{{ version.message || 'no message' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isLimited"
|
||||
class="text-primary inline-block rounded-full pl-1 text-xs font-medium"
|
||||
>
|
||||
{{ version.sourceApplication }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon, LockClosedIcon } from '@heroicons/vue/24/solid'
|
||||
import { keyboardClick } from '@speckle/ui-components'
|
||||
import { LockClosedIcon, EllipsisHorizontalIcon } 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'
|
||||
|
||||
dayjs.extend(localizedFormat)
|
||||
@@ -115,6 +130,8 @@ const props = withDefaults(
|
||||
showTimeline?: boolean
|
||||
last: boolean
|
||||
lastLoaded: boolean
|
||||
modelId?: string
|
||||
totalVersions?: number
|
||||
}>(),
|
||||
{
|
||||
clickable: true,
|
||||
@@ -128,6 +145,7 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{
|
||||
(e: 'changeVersion', version: string): void
|
||||
(e: 'viewChanges', version: ViewerModelVersionCardItemFragment): void
|
||||
(e: 'removeVersion', versionId: string): void
|
||||
}>()
|
||||
|
||||
const mp = useMixpanel()
|
||||
@@ -154,6 +172,36 @@ const createdAt = computed(() => {
|
||||
|
||||
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: '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)
|
||||
@@ -171,4 +219,16 @@ const handleViewChanges = () => {
|
||||
action: 'enable'
|
||||
})
|
||||
}
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem }) => {
|
||||
const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case 'remove-version':
|
||||
if (canDeleteVersion.value) {
|
||||
emit('removeVersion', props.version.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user